A Go daemon to automatically manage power profiles on the Framework Desktop, specifically tailored for hybrid setups acting as both a Steam Gaming Console and an AI Server.
Important
This daemon is designed for Framework Desktop usage and has been explicitly tested on CachyOS. It ensures the system runs at peak performance when gaming (Active or Remote Play) and drastically reduces power consumption during idle AI server operation (24/7).
Tip for AI/Ollama Users: When running local LLMs, using ollama-vulkan has been observed to save ~20W/h compared to ollama-rocm on this hardware.
- Automated Power Management: Switches to "Performance" during active usage (Gaming/Input) and "Powersave" during inactivity.
- Idle Detection: Monitors raw input activity (default 5-minute timeout). Support for Joystick Deadzones and Event Deduplication to prevent drift.
- Game Pausing: Recursively pauses the entire process tree of a Steam game (including Proton/Wine wrappers) when idle. Syncs state on startup to prevent conflicts.
- Steam Remote Play Detection: Detects virtual input devices from Remote Play (specifically
jsinterfaces) to keep the system active. - Ollama Usage Monitoring: Tracks API usage per IP/group with cost accounting (energy × €/kWh) via journald log parsing.
- Ollama Model Tracking: Monitors loaded models and VRAM usage via Ollama's
/api/psendpoint. - GPU Monitoring: Tracks AMD GPU metrics via sysfs (VRAM, GTT, temperature, power) and system CPU usage via
/proc/stat. - SMART Health Monitoring: Monitors SMART health alerts via smartd journald logs for hard drive failure detection.
- REST API: Allows manual mode overriding, idle resetting, and usage statistics.
- JWT Authentication: Secure API access with JSON Web Tokens.
- Configuration File: JSON-based daemon configuration.
- Systemd Integration: Runs effectively as a background service.
The daemon operates by listening to multiple sources of input: Kernel Udev events (HDMI), Input Devices (Activity), Steam Processes, and Virtual Devices (Remote Play).
graph TD
subgraph Sources [Input Sources]
Input[Raw Input Monitor]
Steam[Steam Process Check]
Remote[Virtual Joystick Check]
Turbo[Turbostat Power Monitor]
Ollama[Ollama Log Monitor]
GPU[GPU Sysfs Monitor]
SMART[SMARTd Journal Monitor]
API[User API]
end
subgraph Logic [State Machine]
Main{Power Daemon}
end
subgraph Actions [Power Management]
PCTL[powerprofilesctl]
Pauser[Recursive Process Pauser]
end
Input -->|Activity| Main
Steam -->|Game PID| Main
Remote -->|Virtual Device| Main
Turbo -->|Power/Energy Stats| Main
Ollama -->|Usage/Cost Stats| Main
GPU -->|GPU Metrics| Main
SMART -->|Health Alerts| Main
API -->|Override/Activity| Main
Main -->|Set Profile| PCTL
Main -->|SIGSTOP/SIGCONT| Pauser
Input -.->|fsnotify| Hotplug[Device Hotplug]
Hotplug --> Input
Ollama -.->|journald| Journal[systemd Journal]
GPU -.->|sysfs| Sysfs[/sys/class/drm]
SMART -.->|journald| Journal
The daemon uses turbostat to provide accurate, real-time power consumption metrics.
- Source:
turbostat(running viasudo, reading MSRs). - Metrics:
- Package: Total package power.
- Core: CPU Core power.
- RAM: Memory power.
- Total Energy: Calculated as the SUM of
PkgWatt+CorWatt+RAMWattto account for all reported sensors as requested. - History: Tracks 24-hour and 7-day rolling energy consumption in kWh.
Network devices (Wi-Fi and Ethernet) are explicitly excluded from Runtime Power Management (forced to on) to prevent connection drops and instability. This is particularly important for chipsets like the MediaTek MT7925 (Wi-Fi 7) which are known to be unstable with powertop --auto-tune or aggressive ASPM. The status of these devices can be verified via the /status API endpoint.
The following flowchart illustrates how the daemon determines which power profile to apply. Active Gaming (either Local Input or Remote Play + Running Game) takes priority.
flowchart TD
Start([State Change Event]) --> CheckActive{Is Game Running?}
CheckActive -- Yes --> CheckRemote{Is Remote Play?}
CheckRemote -- Yes --> ForcePerf["Force Performance (Ignore Idle)"]
CheckRemote -- No --> CheckInput{Input Detected?}
CheckActive -- No --> CheckInput
CheckInput -- Yes --> ForcePerf
CheckInput -- No --> CheckIdle{Is System Idle?}
CheckIdle -- Yes --> Suspend["Pause Game Tree & Set Power Saver"]
CheckIdle -- No --> Resume["Resume Game Tree & Set Performance"]
Resume --> ForcePerf
- Monitoring:
- Remote Play: Polls
/proc/bus/input/devicesto detect virtual joysticks. - Input: Monitors
/dev/input/js*(with deadzone) and valid/dev/input/event*devices. Ignores noise and init bursts. - Steam: Periodically checks for running Steam games and identifies the "Reaper" root process.
- Remote Play: Polls
Note
For a detailed explanation of the idle detection mechanism, including diagrams, see Idle Detection Logic.
- Decision Making:
- Priority 1: Active Usage.
- Local Input: Moving mouse/keyboard/controller -> Performance.
- Remote Play: IF Remote Play is active AND a Game is running -> Performance (Ignores Idle).
- Priority 2: Idle (No Active Usage).
- Action: Force Power Saver. If a game is running, Recursively Pause It (targeting
wineserveror game process, avoiding Steamreaper).
- Action: Force Power Saver. If a game is running, Recursively Pause It (targeting
- Priority 1: Active Usage.
- Action:
- Manages Power Profiles and Process States (Running/Stopped).
The daemon relies on the following tools:
powerprofilesctl: For changing system power profiles.turbostat: For accurate power monitoring (usually part oflinux-toolsorlinux-cpupower).libsystemd-dev: Required for building (Ollama journald integration uses cgo).powertop(Optional): For auto-tuning power parameters.scxctl(Optional): For sched-ext scheduler management.
For AMD GPU metrics, ensure your GPU is accessible via sysfs:
/sys/class/drm/card*/device/must containmem_info_vram_*,mem_info_gtt_*- For temperature/power:
hwmoninterface must be present
Configure smartd to log to journald. Add to /etc/smartd.conf:
/dev/sda -a -H -l error -l selftest -m root
Or for NVMe:
/dev/nvme0n1 -a -W 4,45,50 -m root
-
Clone the repository:
git clone https://github.com/zaolin/framework-powerd.git cd framework-powerd -
Build:
go build ./cmd/framework-powerd
-
Install Binary:
sudo cp framework-powerd /usr/local/bin/
-
Install Service:
sudo cp configs/systemd/framework-powerd.service /etc/systemd/system/ sudo systemctl daemon-reload sudo systemctl enable --now framework-powerd
You can customize the daemon's behavior with flags:
# Debug mode (verbose logs)
framework-powerd serve --debug
# Custom Idle Timeout (default 5m)
framework-powerd serve --idle-timeout 30sThis project is compatible with HACS (Home Assistant Community Store).
Installation via HACS:
- Open HACS in Home Assistant.
- Click the menu (three dots) in the top right corner and select Custom repositories.
- Add the repository URL:
https://github.com/zaolin/framework-powerd. - Select Integration as the Category and click Add.
- Find Framework Power Daemon in the list and click Download.
- Restart Home Assistant.
- Go to Settings > Devices & Services > Add Integration.
- Search for Framework Power Daemon and configure it.
- Host: IP address of the daemon (e.g.
192.168.1.xorlocalhostif on same machine). - Port:
8080(default). - JWT Secret: If you started the daemon with a secret.
- Host: IP address of the daemon (e.g.
Sensors Provided:
- Power Mode:
performance/powersave - System Idle:
True/False - Game Status: PID and Running/Paused state
- Power Usage: Package, Core, RAM (Watts)
- Energy Consumption: Last 24h & 7 Days (kWh, Sum of Pkg+Cor+RAM)
- Uptime: System uptime (duration)
- Polling Interval: Configurable number entity (seconds)
- GPU Metrics (if enabled):
- GPU Temperature (°C)
- GPU Power (Watts)
- GPU VRAM Used/Total (GB)
- GPU GTT Used (MB)
- CPU Usage (%)
- SMART Health (if enabled):
- SMART Alert (Binary Sensor)
- SMART Alerts Count
- Ollama Per-Group (if enabled):
- Requests count
- Energy consumption (kWh)
- Cost (€ or configured currency)
- Ollama Models (if enabled):
- Loaded Models (list of model names)
- Ollama VRAM Usage (GB)
Trigger modes manually using the REST API (default port 8080).
If JWT authentication is enabled, you must export your token first:
export TOKEN="your_jwt_token_here"-
Set Performance:
curl -H "Authorization: Bearer $TOKEN" -X POST -d '{"mode":"performance"}' http://localhost:8080/mode
-
Set Powersave:
curl -H "Authorization: Bearer $TOKEN" -X POST -d '{"mode":"powersave"}' http://localhost:8080/mode
-
Trigger Activity (Reset Idle Timer):
curl -H "Authorization: Bearer $TOKEN" -X POST http://localhost:8080/activity -
Get Status:
curl -H "Authorization: Bearer $TOKEN" http://localhost:8080/status -
Get Ollama Stats (if enabled):
curl -H "Authorization: Bearer $TOKEN" http://localhost:8080/ollama/stats # Output: {"by_ip":{...},"by_group":{"lan":{"count":5,"total_cost":0.0012},...}}
-
Get SMART Stats (if enabled):
curl -H "Authorization: Bearer $TOKEN" http://localhost:8080/smartd/stats # Output: {"alerts":[{"device":"/dev/sda","fail_type":"SelfTest",...}]}
To enable JWT authentication, start the daemon with the --jwt-secret flag:
/usr/local/bin/framework-powerd serve --jwt-secret="mysecret"To generate a token:
/usr/local/bin/framework-powerd token --secret="mysecret"Use the token in your requests:
export TOKEN=$(/usr/local/bin/framework-powerd token --secret="mysecret")
curl -H "Authorization: Bearer $TOKEN" http://localhost:8080/statusThe daemon supports a JSON configuration file. Create /etc/framework-powerd/config.json:
{
"server": {
"address": "0.0.0.0",
"port": 8080,
"idle_timeout": "5m"
},
"ollama": {
"enabled": true,
"service_unit": "ollama.service",
"groups": [
{"name": "lan", "cidrs": ["192.168.0.0/16"]},
{"name": "tailscale", "cidrs": ["100.64.0.0/10"]}
]
},
"smartd": {
"enabled": true,
"service_unit": "smartd.service",
"notify_service": "notify.mobile_phone",
"alert_retention": "30s"
},
"gpu": {
"enabled": true,
"poll_interval": "5s"
},
"pricing": {
"energy_price_per_kwh": 0.32,
"currency": "EUR"
}
}| Section | Option | Type | Default | Description |
|---|---|---|---|---|
server.address |
Address | string | localhost |
IP to bind to |
server.port |
Port | int | 8080 |
HTTP port |
server.idle_timeout |
Idle Timeout | duration | 5m |
Time before idle |
ollama.enabled |
Enabled | bool | false |
Enable Ollama monitoring |
smartd.enabled |
Enabled | bool | false |
Enable SMART monitoring |
smartd.notify_service |
Notify Service | string | "" |
HA notify service |
smartd.alert_retention |
Alert Retention | duration | 30s |
How long to keep alerts |
gpu.enabled |
Enabled | bool | false |
Enable GPU monitoring |
gpu.poll_interval |
Poll Interval | duration | 5s |
GPU polling frequency |
Run with config:
/usr/local/bin/framework-powerd serve --config=/etc/framework-powerd/config.jsonCLI flags override config file values:
--address: The IP address to listen on (default:localhost). Use0.0.0.0to listen on all interfaces.--port: The port to listen on (default:8080).
Note: If you change the port or address, remember to update your API calls accordingly.
