Es gibt diese kleinen Probleme, die einen jahrelang unterschwellig nerven, bis man sie endlich einmal sauber löst. Genau in diese Kategorie fällt bei mir das Thema „Was macht das System, wenn ich auf Akku bin – und was darf es nur am Netz?“
Klar, es gibt Tools wie TLP, Powertop & Co., und viele Desktop-Umgebungen haben ihre eigenen Energiespar-Profile. Aber ich wollte etwas anderes:
Ich wollte den aktuellen Power-Zustand (Akku vs. Netz) als systemd-Target haben – sauber integriert, automatisierbar, paketiert und über meine eigene Debian-Repository-Infrastruktur ausrollbar.
Das Ergebnis ist dieses kleine, aber sehr praktische Projekt: power-state-target.
Problem: Der Energiezustand als „unsichtbare“ Information
UPower weiß, ob die Maschine auf Akku läuft. Der Desktop weiß es. Der Nutzer sieht irgendwo ein Batteriesymbol. Aber für viele meiner eigenen Dienste, Timer und Skripte ist diese Info nicht direkt greifbar.
Typische Fragen:
- „Starte diesen Dienst nur, wenn das Gerät am Netz hängt.“
- „Fahre diese IO-intensiven Jobs bitte nicht auf Akku.“
- „Auf Akku darf etwas weiterlaufen, aber nur in einem Light-Mode.“
Man kann natürlich überall lokale Checks einbauen – upower, busctl, eigene Daemons, irgendwelche kleinen Wrapper. Aber das ist alles verteilt, unübersichtlich und schlecht testbar.
Systemd bietet für solche Zustände eigentlich eine perfekte Abstraktion: Targets.
Also war der Plan:
- Ich möchte ein
power-ac.targetund einpower-battery.targethaben. - Genau einer dieser Targets soll immer „aktiv“ sein – je nachdem, ob das System auf Akku oder am Netz hängt.
- Dienste können sich dann einfach daran „dranhängen“, statt selbst Power-Logik zu implementieren.
Ziel: systemd als Single Source of Truth für den Power-State
Die Idee hinter power-state-target ist bewusst minimalistisch:
- Keine eigene fette Daemon-Logik.
- Nutzung von vorhandener Infrastruktur: UPower und systemd.
- Ein kleiner Listener, der den Zustand aus UPower liest und passende systemd-Targets setzt.
- Das Ganze als Debian-Paket, das sich in meine bestehende Build- und Release-Infrastruktur einfügt.
Im Kern besteht das Projekt aus:
- zwei systemd-Targets:
power-ac.targetundpower-battery.target - einem systemd-Service:
power-state-listener.service - einem kleinen Shell-Skript:
/usr/libexec/update-power-state - Debian-Packaging + automatisierter Build- und Release-Pipeline über Gitea
Die Targets: power-ac und power-battery
Die beiden Targets sind bewusst extrem simpel gehalten. Sie dienen als Zustandsmarkierungen, nicht als eigene Logik-Einheiten:
| |
| |
Ein paar Gedanken dazu:
- Sie haben keine eigenen
[Install]-Sektionen – sie werden nicht direkt aktiviert, sondern vom Listener-Service gesetzt. - Über
AllowIsolate=yeskann man sie theoretisch auch isolieren, falls man das einmal für Experimente nutzen möchte. - Für andere Units sind sie vor allem als Anker gedacht:
Wants=power-ac.target,BindsTo=power-battery.target,PartOf=usw.
Die eigentliche Intelligenz steckt im Listener.
Der Listener: power-state-listener.service
Der Listener ist ein kleiner systemd-Service, der genau eine Aufgabe hat:
UPower beobachten und bei Änderungen der Energiequelle das passende Target aktivieren.
Die Unit dazu sieht so aus:
| |
Die wichtigen Punkte:
ExecStartPre=/usr/libexec/update-power-statesorgt dafür, dass der aktuelle Zustand sofort beim Start des Dienstes gesetzt wird (z.B. nach einem Boot).ExecStart=... upower --monitor-detailhängt sich an die UPower-Monitor-API und wartet auf ein Ereignis (zum Beispiel: Stromkabel gezogen oder eingesteckt).- Sobald ein Ereignis eintritt, läuft danach erneut
update-power-state, dann beendet sich der Dienst erfolgreich. - Durch
Restart=on-successwird der Service automatisch neu gestartet. Ergebnis: eine kleine Ereignis-Schleife, die immer reagiert, wenn sich der Power-State ändert, ohne selbst busy zu sein.
Das ist im Grunde ein reaktives Mini-Framework: systemd + UPower erledigen 95 % der Arbeit, der Rest ist eine dünne Klebeschicht.
Die eigentliche Logik: /usr/libexec/update-power-state
Das Herzstück ist ein Shell-Skript, das genau zwei Dinge tut:
- Den aktuellen UPower-Status („bin ich auf Akku?“) abfragen.
- Die passenden systemd-Targets setzen.
| |
Ein paar Details dazu:
busctl get-property ... OnBatteryholt sich direkt denOnBattery-Property von UPower über D-Bus.- UPower liefert hier ein
b trueoderb false– überawk '{print $2}'ziehe ich mir einfach den Wahrheitswert heraus. - Wenn
true, wirdpower-battery.targetgestartet undpower-ac.targetgestoppt. - Wenn
false, genau umgekehrt.
Damit ist sichergestellt, dass nie beide Targets gleichzeitig aktiv sind, sondern immer genau einer der beiden Zustände gilt.
Integration in Debian: Paket power-state-target
Weil ich praktisch alles, was ich produktiv nutzen will, in .deb-Pakete verpacke, war von Anfang an klar: Das hier wird ein normales Debian-Paket mit sauberem debian/-Verzeichnis.
Die debian/control ist bewusst minimal:
| |
- Architektur ist
all, weil es sich nur um Skripte und Unit-Files handelt. - Die einzige echte Laufzeit-Abhängigkeit ist
upower. Rules-Requires-Root: no– gebaut werden kann also auch ohne Root, sofern die Build-Dependencies vorhanden sind.
Die Installation der Dateien übernimmt debian/install:
| |
debian/rules ist dementsprechend trivial:
| |
Das Ganze ist als native Paketquelle (debian/source/format = 3.0 (native)) angelegt, da Projekt und Paket faktisch identisch sind.
Automatisierter Debian-Build mit build.sh
Um das Paket nicht nur lokal, sondern auch automatisiert im CI bauen zu können, gibt es ein eigenes Build-Skript:
| |
Wichtige Punkte:
- Das Script kümmert sich darum, dass alle benötigten Build-Pakete installiert sind (typische
build-essential/devscripts/debhelper-Kette). - Die Versionierung und der Changelog werden über ein externes Script aus meiner
deb-changelog-actionautomatisiert. - Alle entstehenden Artefakte werden in ein zentrales
dist/-Verzeichnis verschoben, sodass der CI-Job sie leicht weiterverarbeiten kann.
TAG_NAME kommt dabei aus dem CI-Kontext – das ist die Git-Tag-Version, die zum Release gehört.
CI/CD: Vom Commit zum Debian-Paket in der eigenen Repo
Da das Projekt auf meiner privaten Gitea-Instanz liegt, laufen die CI-Pipelines ebenfalls dort. Es gibt zwei wesentliche Workflows:
release.yml– erstellt automatisch Releases und Changelogs.build.yml– baut aus einem Release ein Debian-Paket und schiebt es in meinedebrepo.
Auto-Changelog & Release
Der Release-Workflow triggert bei Pushes auf main:
| |
Die auto-changelog-release-action kümmert sich darum, anhand der Commits (per git-cliff-Konfiguration) einen Changelog zu erzeugen, eine neue Version zu bestimmen, den Tag zu setzen und das Release im Gitea-Repository zu veröffentlichen.
Debian-Build und Push in debrepo
Sobald ein Release veröffentlicht ist, greift der zweite Workflow:
| |
Der Job:
- checkt den Quellcode aus,
- ruft
build.shmit dem Tag aus dem Release auf, - macht ein Sparse Checkout meiner
debrepo(nurpower-state-target/), - kopiert die
.deb,.dscund Tarballs in dieses Verzeichnis, - commitet neue Artefakte auf den
packages-Branch.
Damit landet jede neue Version von power-state-target automatisch in meiner eigenen APT-Repository-Struktur – und kann dann wie gewohnt über apt installiert werden.
git-cliff: Konsistente Changelogs mit Emojis
Da ich inzwischen sehr viel Wert auf saubere, konsistente Changelogs lege, kommt auch hier wieder git-cliff zum Einsatz. Die cliff.toml ist an meinen üblichen Stil angepasst – inklusive Emoji-Gruppierung:
- 🚀 Features
- 🐛 Bug Fixes
- ⚙️ Miscellaneous Tasks
- usw.
Für power-state-target sieht der erste Release-Eintrag aktuell so aus:
| |
Nichts weltbewegendes, aber genau das ist der Punkt: saubere kleine Bausteine, die später zusammenspielen können.
Wie man power-state-target praktisch nutzt
Der eigentliche Mehrwert entsteht dadurch, dass andere Units auf die Targets reagieren können.
Ein paar typische Szenarien:
1. Dienste nur am Netz erlauben
Angenommen, du hast einen Dienst, der nur laufen soll, wenn das System am Netz hängt, z.B. ein Backup- oder Dedupe-Job.
| |
Hier kann man zusätzlich mit ConditionPathExists, Timer-Units, StartLimit* usw. arbeiten – aber der Kern ist: „nur sinnvoll, wenn AC da ist“.
2. Reduzierter Modus auf Akku
Statt Dienste komplett abzuschalten, kann man auch alternative Konfigurationen fahren, z.B. einen „Light-Modus“ auf Akku und „Full-Modus“ am Netz. Man modelliert dann zwei Units, die jeweils an ein anderes Target gekoppelt sind.
3. Timer, die auf den Power-State hören
Timer-Units können ebenfalls indirekt über die Targets gesteuert werden, etwa indem nur der eigentliche Service an das passende Target gebunden wird.
Das Schöne: Alle Entscheidungen laufen über systemd, nicht über wilde Shell-Ifs überall im System.
Ausblick: Was man darauf noch aufbauen kann
power-state-target ist bewusst klein gehalten. Aber genau dadurch lässt es sich gut als Baustein für komplexere Setups verwenden:
- Dynamisches Umschalten des CPU-Governors über separate Dienste.
- Anpassung von Backup-Strategien (z.B. nur Metadaten auf Akku, Voll-Backups am Netz).
- Steuerung von Container- oder VM-Last abhängig vom Power-State.
- Integration in bestehende Tuning-Tools, indem diese die Targets nur noch als Signalquelle verwenden.
Ich mag solche Projekte, weil sie zwei Welten verbinden:
- Auf der einen Seite: klassische Desktop-/Laptop-Welt mit Akku, UPower, grafischen Energiemanagern.
- Auf der anderen: systemd- und serverartige Steuerung, bei der Zustände explizit modelliert sind.
power-state-target macht den Energiezustand zu einem ersten Bürger in systemd – und damit für alles Weitere komponierbar.
Fazit
Für sich genommen ist power-state-target ein kleines Projekt:
- ein Shell-Skript,
- zwei Targets,
- ein Service,
- etwas Debian-Packaging und CI.
Aber für mich ist es genau die Art von Infrastruktur, die ich mag:
- klar umrissen,
- gut integrierbar,
- automatisiert gebaut und ausgeliefert,
- und mit einem einfachen mentalen Modell: „Bin ich auf Akku oder am Netz?“ wird zu „Welches systemd-Target ist aktiv?“
Und ab da kann der Rest des Systems sich darauf verlassen.
