Initial release of KDE Power Monitor plasmoid

This commit is contained in:
enne2
2026-09-08 22:33:14 +02:00
commit cfe2c1a9c3
7 changed files with 586 additions and 0 deletions
+9
View File
@@ -0,0 +1,9 @@
# Plasma / Qt user artefacts
*.plasmoid
*.qmlc
*.jsc
# Editor and OS
*~
*.swp
.DS_Store
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 enne2
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+42
View File
@@ -0,0 +1,42 @@
# KDE Power Monitor Plasmoid
Plasmoide per **KDE Plasma 6** che mostra in tempo reale:
- utilizzo, temperatura e potenza RAPL della CPU;
- utilizzo, temperatura, frequenza e PPT dell'iGPU AMD;
- RAM disponibile/usata e swap;
- storico di utilizzo di CPU, GPU e RAM.
## Requisiti
- KDE Plasma 6;
- Linux con `/proc/stat`, `/proc/meminfo` e sensori `hwmon`;
- per la potenza CPU: RAPL leggibile dall'utente.
Su sistemi dove RAPL è root-locked, può essere resa leggibile in modo persistente con una regola `systemd-tmpfiles`, ad esempio:
```text
z /sys/class/powercap/intel-rapl:*/energy_uj 0444 root root -
```
## Installazione
```bash
kpackagetool6 -t Plasma/Applet -i .
```
Per aggiornare un'installazione esistente:
```bash
kpackagetool6 -t Plasma/Applet -u .
```
Aggiungi poi **Power Monitor** al desktop o a un pannello dalle impostazioni dei widget Plasma.
## Dati e note
Il backend `contents/scripts/power.sh` legge solo procfs/sysfs. La potenza dell'iGPU è etichettata come **PPT APU**: non viene sommata alla potenza package RAPL della CPU perché i domini possono sovrapporsi su un'APU.
## Licenza
MIT. Vedi [LICENSE](LICENSE).
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

+47
View File
@@ -0,0 +1,47 @@
#!/bin/sh
STATE="/tmp/.powermonitor-state"
PTOT=""; PIDLE=""; PJ=""; PMS=""; PMAX=""
[ -r "$STATE" ] && . "$STATE" 2>/dev/null
v=$(awk '/^cpu /{print $2+$3+$4+$5+$6+$7+$8+$9, $5+$6}' /proc/stat)
TOT=${v%% *}; IDLE=${v##* }; NOW=$(date +%s%3N)
CPU_PCT=""
if [ -n "$PTOT" ] && [ "$TOT" -gt "$PTOT" ]; then
DT=$((TOT-PTOT)); DI=$((IDLE-PIDLE)); [ "$DT" -gt 0 ] && CPU_PCT=$((100*(DT-DI)/DT))
fi
GPU_UW=""; GPU_AVG_UW=""; GPU_TEMP=""; CPU_TEMP=""; GPU_HZ=""
for h in /sys/class/hwmon/hwmon*; do
n=$(cat "$h/name" 2>/dev/null)
if [ "$n" = amdgpu ]; then
[ -r "$h/power1_input" ] && GPU_UW=$(cat "$h/power1_input")
[ -r "$h/power1_average" ] && GPU_AVG_UW=$(cat "$h/power1_average")
[ -r "$h/temp1_input" ] && GPU_TEMP=$(cat "$h/temp1_input")
[ -r "$h/freq1_input" ] && GPU_HZ=$(cat "$h/freq1_input")
elif [ "$n" = k10temp ] && [ -r "$h/temp1_input" ]; then CPU_TEMP=$(cat "$h/temp1_input"); fi
done
GPU_BUSY=""
for c in /sys/class/drm/card[0-9]; do
if [ "$(readlink "$c/device/driver" 2>/dev/null | xargs -r basename)" = amdgpu ] && [ -r "$c/device/gpu_busy_percent" ]; then GPU_BUSY=$(cat "$c/device/gpu_busy_percent"); break; fi
done
E_UJ=$(cat /sys/class/powercap/intel-rapl:0/energy_uj 2>/dev/null); E_MAX=$(cat /sys/class/powercap/intel-rapl:0/max_energy_range_uj 2>/dev/null)
CPU_W100=""
if [ -n "$PJ" ] && [ -n "$E_UJ" ] && [ -n "$PMS" ]; then
DUJ=$((E_UJ-PJ)); DMS=$((NOW-PMS))
if [ "$DUJ" -lt 0 ] && [ -n "$PMAX" ] && [ "$DUJ" -lt -$((PMAX/2)) ]; then DUJ=$((DUJ+PMAX)); fi
if [ "$DUJ" -ge 0 ] && [ "$DMS" -gt 0 ] && [ "$DMS" -lt 60000 ]; then CPU_W100=$((DUJ/(DMS*10))); [ "$CPU_W100" -gt 9999 ] && CPU_W100=9999; fi
fi
RAM_TOTAL=$(awk '/^MemTotal:/{print $2}' /proc/meminfo)
RAM_AVAILABLE=$(awk '/^MemAvailable:/{print $2}' /proc/meminfo)
SWAP_TOTAL=$(awk '/^SwapTotal:/{print $2}' /proc/meminfo)
SWAP_FREE=$(awk '/^SwapFree:/{print $2}' /proc/meminfo)
RAM_USED=$((RAM_TOTAL-RAM_AVAILABLE)); SWAP_USED=$((SWAP_TOTAL-SWAP_FREE))
echo "cpu_usage_pct=${CPU_PCT}"; echo "cpu_temp_mdegC=${CPU_TEMP}"; echo "cpu_power_cw=${CPU_W100}"
echo "gpu_busy_pct=${GPU_BUSY}"; echo "gpu_temp_mdegC=${GPU_TEMP}"; echo "gpu_power_uw=${GPU_UW}"; echo "gpu_power_avg_uw=${GPU_AVG_UW}"; echo "gpu_freq_hz=${GPU_HZ}"
echo "ram_total_kb=${RAM_TOTAL}"; echo "ram_used_kb=${RAM_USED}"; echo "ram_available_kb=${RAM_AVAILABLE}"; echo "swap_total_kb=${SWAP_TOTAL}"; echo "swap_used_kb=${SWAP_USED}"
echo "ts_ms=${NOW}"
cat > "$STATE" <<EOF
PTOT=$TOT
PIDLE=$IDLE
PJ=$E_UJ
PMS=$NOW
PMAX=$E_MAX
EOF
+449
View File
@@ -0,0 +1,449 @@
import QtQuick
import org.kde.plasma.plasmoid
import org.kde.plasma.core as PlasmaCore
import org.kde.plasma.components 3.0 as PlasmaComponents
import org.kde.plasma.plasma5support as P5Support
import org.kde.kirigami as Kirigami
PlasmoidItem {
id: root
readonly property int updateMs: 1000
readonly property color cpuAccent: "#55c5c0"
readonly property color gpuAccent: "#9876d9"
readonly property color ramAccent: "#d9a84e"
property real cpuUsage: NaN
property real cpuTemp: NaN
property real cpuW: NaN
property real gpuUsage: NaN
property real gpuTemp: NaN
property real gpuW: NaN
property real gpuWAvg: NaN
property real gpuMhz: NaN
property real ramUsed: NaN
property real ramTotal: NaN
property real ramAvailable: NaN
property real swapUsed: NaN
property real swapTotal: NaN
property var cpuHistory: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
property var gpuHistory: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
property var ramHistory: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
signal metricsUpdated()
// Come gli altri dashboard KDE: sfondo semitrasparente nativo Plasma.
Plasmoid.backgroundHints: PlasmaCore.Types.DefaultBackground
preferredRepresentation: fullRepresentation
P5Support.DataSource {
engine: "executable"
interval: root.updateMs
connectedSources: [root.script]
onNewData: function(name, data) {
if (data["exit code"] === 0) root.parse(data["stdout"] || "")
}
}
readonly property string script: Qt.resolvedUrl("../scripts/power.sh").toString().replace("file://", "")
function number(values, key, divisor) {
const value = parseFloat(values[key])
return isNaN(value) ? NaN : value / (divisor || 1)
}
function appendSample(samples, value) {
const copy = samples.slice(0)
copy.push(isNaN(value) ? 0 : value)
if (copy.length > 24) copy.shift()
return copy
}
function parse(output) {
const values = {}
output.split("\n").forEach(function(line) {
const separator = line.indexOf("=")
if (separator > 0) values[line.slice(0, separator)] = line.slice(separator + 1)
})
cpuUsage = number(values, "cpu_usage_pct")
cpuTemp = number(values, "cpu_temp_mdegC", 1000)
cpuW = number(values, "cpu_power_cw", 100)
gpuUsage = number(values, "gpu_busy_pct")
gpuTemp = number(values, "gpu_temp_mdegC", 1000)
gpuW = number(values, "gpu_power_uw", 1000000)
gpuWAvg = number(values, "gpu_power_avg_uw", 1000000)
gpuMhz = number(values, "gpu_freq_hz", 1000000)
ramTotal = number(values, "ram_total_kb")
ramUsed = number(values, "ram_used_kb")
ramAvailable = number(values, "ram_available_kb")
swapTotal = number(values, "swap_total_kb")
swapUsed = number(values, "swap_used_kb")
cpuHistory = appendSample(cpuHistory, cpuUsage)
gpuHistory = appendSample(gpuHistory, gpuUsage)
ramHistory = appendSample(ramHistory, ramPercent())
metricsUpdated()
}
function clampPercent(value) {
return isNaN(value) ? 0 : Math.max(0, Math.min(100, value))
}
function ramPercent() {
return ramTotal > 0 ? ramUsed / ramTotal * 100 : NaN
}
function formatNumber(value, decimals) {
return isNaN(value) ? "—" : value.toFixed(decimals === undefined ? 0 : decimals)
}
function watts(value) {
return isNaN(value) ? "n/d" : value.toFixed(1) + " W"
}
function gibibytes(kilobytes) {
return isNaN(kilobytes) ? "—" : (kilobytes / 1048576).toFixed(1) + " GiB"
}
fullRepresentation: Rectangle {
id: dashboard
readonly property bool narrow: width < 520
implicitWidth: 570
implicitHeight: narrow ? 650 : 368
radius: 14
color: "transparent"
border.color: "transparent"
border.width: 0
Column {
anchors.fill: parent
anchors.margins: 16
spacing: 10
// Due righe esplicite: nessuna collisione tra titolo e stato LIVE.
Column {
width: parent.width
height: 39
spacing: 3
Row {
spacing: 9
Rectangle {
width: 4
height: 18
radius: 2
color: root.cpuAccent
}
PlasmaComponents.Label {
text: "POWER MONITOR"
color: "#e8edf4"
font.bold: true
font.pointSize: dashboard.narrow ? 11 : 13
font.letterSpacing: 0.8
}
}
Item {
width: parent.width
height: 14
Row {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
spacing: 6
Rectangle {
width: 6
height: 6
radius: 3
color: "#62c98b"
}
PlasmaComponents.Label {
text: "LIVE / 1 s"
color: "#8293a8"
font.family: "monospace"
font.pointSize: 8
}
}
}
}
Grid {
id: metricsGrid
width: parent.width
columns: dashboard.narrow ? 1 : 3
spacing: 8
MetricCard {
width: dashboard.narrow ? metricsGrid.width : (metricsGrid.width - 16) / 3
height: dashboard.narrow ? 105 : 126
title: "CPU"
value: root.formatNumber(root.cpuUsage) + "%"
detail: root.watts(root.cpuW) + " / " + (isNaN(root.cpuTemp) ? "n/d" : root.formatNumber(root.cpuTemp) + " °C")
accent: root.cpuAccent
progress: root.clampPercent(root.cpuUsage)
metricIcon: "cpu"
}
MetricCard {
width: dashboard.narrow ? metricsGrid.width : (metricsGrid.width - 16) / 3
height: dashboard.narrow ? 105 : 126
title: "iGPU 780M"
value: root.formatNumber(root.gpuUsage) + "%"
detail: root.watts(root.gpuWAvg) + " / " + (isNaN(root.gpuTemp) ? "n/d" : root.formatNumber(root.gpuTemp) + " °C")
accent: root.gpuAccent
progress: root.clampPercent(root.gpuUsage)
metricIcon: "video-display"
}
MetricCard {
width: dashboard.narrow ? metricsGrid.width : (metricsGrid.width - 16) / 3
height: dashboard.narrow ? 105 : 126
title: "RAM"
value: root.formatNumber(root.ramPercent()) + "%"
detail: root.gibibytes(root.ramUsed) + " / " + root.gibibytes(root.ramTotal)
accent: root.ramAccent
progress: root.clampPercent(root.ramPercent())
metricIcon: "media-flash-memory"
}
}
Flow {
width: parent.width
height: childrenRect.height
spacing: 7
StatChip { label: "CPU"; metric: root.watts(root.cpuW); accent: root.cpuAccent }
StatChip { label: "PPT APU"; metric: root.watts(root.gpuW); accent: root.gpuAccent }
StatChip { label: "FREE"; metric: root.gibibytes(root.ramAvailable); accent: root.ramAccent }
StatChip { label: "SWAP"; metric: root.gibibytes(root.swapUsed) + " / " + root.gibibytes(root.swapTotal); accent: "#7187a4" }
}
Rectangle {
width: parent.width
height: dashboard.narrow ? 118 : 104
radius: 8
color: Qt.rgba(0, 0, 0, 0.25)
border.color: Qt.rgba(0.40, 0.48, 0.58, 0.40)
border.width: 1
Column {
anchors.fill: parent
anchors.margins: 10
spacing: 5
Item {
width: parent.width
height: 18
PlasmaComponents.Label {
anchors.left: parent.left
text: dashboard.narrow ? "HISTORY" : "UTILIZATION HISTORY"
color: "#8293a8"
font.family: "monospace"
font.pointSize: 8
font.letterSpacing: 0.5
}
Row {
anchors.right: parent.right
spacing: dashboard.narrow ? 5 : 9
Legend { label: "CPU"; accent: root.cpuAccent }
Legend { label: "GPU"; accent: root.gpuAccent }
Legend { label: "RAM"; accent: root.ramAccent }
}
}
Canvas {
id: trend
width: parent.width
height: parent.height - 23
onWidthChanged: requestPaint()
onHeightChanged: requestPaint()
Connections {
target: root
function onMetricsUpdated() { trend.requestPaint() }
}
onPaint: {
const context = getContext("2d")
context.clearRect(0, 0, width, height)
context.lineWidth = 1
context.strokeStyle = "#202b39"
for (let row = 1; row < 4; row++) {
const y = row * height / 4
context.beginPath()
context.moveTo(0, y)
context.lineTo(width, y)
context.stroke()
}
function drawSeries(samples, color) {
if (!samples || samples.length < 2) return
context.lineWidth = 1.5
context.strokeStyle = color
context.beginPath()
for (let index = 0; index < samples.length; index++) {
const x = index * width / (samples.length - 1)
const y = height - root.clampPercent(samples[index]) / 100 * height
if (index === 0) context.moveTo(x, y)
else context.lineTo(x, y)
}
context.stroke()
}
drawSeries(root.ramHistory, root.ramAccent)
drawSeries(root.gpuHistory, root.gpuAccent)
drawSeries(root.cpuHistory, root.cpuAccent)
}
}
}
}
}
}
component MetricCard: Rectangle {
id: card
property string title
property string value
property string detail
property color accent
property real progress
property string metricIcon
implicitWidth: 170
implicitHeight: 126
radius: 8
color: Qt.rgba(0, 0, 0, 0.25)
border.color: Qt.rgba(card.accent.r, card.accent.g, card.accent.b, 0.55)
border.width: 1
Rectangle {
anchors { left: parent.left; top: parent.top; bottom: parent.bottom; topMargin: 9; bottomMargin: 9 }
width: 2
radius: 1
color: card.accent
opacity: 0.9
}
Column {
anchors.fill: parent
anchors.margins: 11
spacing: 7
Item {
width: parent.width
height: 27
Row {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
spacing: 6
Rectangle {
width: 24
height: 24
radius: 5
color: Qt.rgba(card.accent.r, card.accent.g, card.accent.b, 0.11)
Kirigami.Icon {
anchors.centerIn: parent
width: 14
height: 14
source: card.metricIcon
}
}
PlasmaComponents.Label {
text: card.title
color: "#b9c4d2"
font.bold: true
font.pointSize: 9
}
}
PlasmaComponents.Label {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
text: card.value
color: card.accent
font.family: "monospace"
font.bold: true
font.pointSize: 18
}
}
Rectangle {
width: parent.width
height: 4
radius: 2
color: "#293649"
Rectangle {
width: parent.width * Math.max(0, Math.min(100, card.progress)) / 100
height: parent.height
radius: 2
color: card.accent
}
}
PlasmaComponents.Label {
width: parent.width
text: card.detail
color: "#899aaf"
font.family: "monospace"
font.pointSize: 8
elide: Text.ElideRight
}
}
}
component StatChip: Rectangle {
id: chip
property string label
property string metric
property color accent
implicitWidth: chipRow.implicitWidth + 16
implicitHeight: 25
radius: 5
color: Qt.rgba(0, 0, 0, 0.25)
border.color: Qt.rgba(0.40, 0.48, 0.58, 0.40)
border.width: 1
Row {
id: chipRow
anchors.centerIn: parent
spacing: 5
Rectangle { width: 4; height: 4; radius: 2; color: chip.accent }
PlasmaComponents.Label { text: chip.label; color: "#72849a"; font.family: "monospace"; font.pointSize: 7 }
PlasmaComponents.Label { text: chip.metric; color: "#c1cad6"; font.family: "monospace"; font.bold: true; font.pointSize: 8 }
}
}
component Legend: Row {
id: legend
property string label
property color accent
spacing: 4
Rectangle { width: 7; height: 2; color: legend.accent; anchors.verticalCenter: parent.verticalCenter }
PlasmaComponents.Label { text: legend.label; color: "#73859b"; font.family: "monospace"; font.pointSize: 7 }
}
compactRepresentation: PlasmaComponents.Label {
text: root.formatNumber(root.cpuUsage) + "% / " + root.formatNumber(root.ramPercent()) + "%"
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
font.family: "monospace"
}
}
+18
View File
@@ -0,0 +1,18 @@
{
"KPackageStructure": "Plasma/Applet",
"KPlugin": {
"Id": "org.enne2.powermonitor",
"Name": "Power Monitor (CPU + iGPU)",
"Description": "Uso percentuale, temperatura e potenza (watt) di CPU e GPU integrata. Legge i sensori ksystemstats e hwmon/RAPL.",
"Icon": "preferences-system-power-management",
"Version": "1.0",
"License": "MIT",
"Authors": [
{
"Name": "enne2"
}
],
"Category": "System Information"
},
"X-Plasma-API-Minimum-Version": "6.0"
}