PowerNap is a formulaic, policy-gated Linux CPU governor controller that adjusts CPU power behaviour based on real-time electricity prices, current CPU load, workload shape, price lookahead, and thermal state.
It is designed for small servers, home labs, and always-on Linux machines where you want lower power use during expensive electricity hours without making the system feel unstable or sluggish.
PowerNap monitors CPU usage, fetches electricity prices from elprisetjustnu.se, stores price and CPU history in SQLite, and changes the active CPU frequency governor when the decision engine determines that price, workload, and safety conditions justify it.
PowerNap v2 uses:
- CLI execution
- systemd logs
- SQLite history
- PrettyTable presentation reports via an explicit command
-
Electricity-price-aware CPU scaling
Uses Swedish electricity price data to avoid unnecessary high-performance CPU behaviour during expensive periods. -
Formulaic decision engine
Uses calculated scores instead of simple fixedif price > x and cpu < ylogic. -
Policy-gated behaviour
Uses a policy matrix as a safe baseline before applying formulaic score adjustments. -
Workload classification
Classifies runtime behaviour into workload types such as idle, light, balanced, burst, sustained heavy, and I/O-bound. -
Price rank classification
Compares the current electricity price against the day’s price range instead of relying only on fixed SEK/kWh thresholds. -
Price lookahead
Looks ahead over upcoming price data so cheap periods before expensive windows can be used more intelligently. -
Thermal safety layer
CPU temperature can reduce aggressiveness, and critical temperature overrides all other decisions. -
Transition gating
Uses minimum hold time, upscale cooldown, and score-change margin to avoid twitchy governor switching. -
Real-time CPU monitoring
Samples per-core CPU usage withpsutiland smooths usage for stable decisions. -
SQLite history
Stores electricity price history, CPU usage history, and governor change events in local SQLite databases. -
Price prefetching
Fetches today’s prices and attempts to fetch tomorrow’s prices after midday when available. -
Resilient API handling
Uses request timeouts, response validation, and exponential backoff with jitter. -
Clean shutdown
HandlesSIGTERMandSIGINT, commits buffered CPU data, and closes databases cleanly. -
PrettyTable report command
Generates readable presentation output using a dedicated--reportcommand. -
Systemd-friendly
Includes an example systemd service file with basic hardening options.
-
Linux with CPU frequency scaling support exposed under:
/sys/devices/system/cpu/cpu*/cpufreq/scaling_governor -
Python 3.10 or newer recommended
-
Python packages:
requests psutil prettytable -
Permission to write to CPU governor files
-
Network access to fetch electricity prices
powernap.py Main PowerNap service script
powernap.conf.example Example configuration file
requirements.txt Python dependencies
powernap.service Example systemd service unit
install.sh Basic install helper
README.md This document
Runtime files created by PowerNap:
prices.db Electricity price history
cpu.db CPU usage history and governor events
Clone the repository:
git clone https://github.com/yourusername/PowerNap.git
cd PowerNapCreate and activate a virtual environment:
python3 -m venv env
source env/bin/activateInstall dependencies:
pip install -r requirements.txtCopy the example configuration:
cp powernap.conf.example powernap.conf
chmod 600 powernap.confRun PowerNap manually:
python3 powernap.pyRun in dry-run mode without writing governors:
python3 powernap.py --dry-runGenerate a presentation report:
python3 powernap.py --reportShow raw diagnostic output:
python3 powernap.py --debugValidate config and environment:
python3 powernap.py --check-configPowerNap is meant to run as a long-running daemon loop.
The daemon loop:
fetches prices
samples CPU usage
calculates workload state
calculates price state
calculates thermal state
builds a decision
applies transition gating
changes governor if needed
writes history to SQLite
sleeps until the next cycle
Reports are separate commands. The daemon does not start a report thread.
The intended model is:
PowerNap daemon process
writes SQLite history
PowerNap report command
reads SQLite history
prints PrettyTable output
exits
SQLite WAL mode is used, so this read/write pattern is suitable for a daemon writing history while a separate report command reads the latest committed data.
PowerNap reads powernap.conf from the script directory by default. You can override the config path with:
POWERNAP_CONFIG=/path/to/powernap.conf python3 powernap.pyExample configuration:
[AreaCode]
AREA = SE3
[Runtime]
SLEEP_INTERVAL = 5
COMMIT_INTERVAL_MINUTES = 5
USAGE_METHOD = median
DRY_RUN = false
[Storage]
DATABASE_DIR = .
[Logging]
LEVEL = INFO
[DataRetention]
ENABLED = true
DAYS = 30
[Network]
REQUEST_TIMEOUT_SEC = 10
[Metrics]
SMOOTH_FACTOR = 0.30
HISTORY_SAMPLES = 24
HIGH_LOAD_THRESHOLD = 70
BURST_CPU_THRESHOLD = 75
IDLE_CPU_THRESHOLD = 15
IOWAIT_THRESHOLD = 25
[PriceStrategy]
CHEAP_RANK_MAX = 0.25
EXPENSIVE_RANK_MIN = 0.80
LOOKAHEAD_HOURS = 3
LOOKAHEAD_DELTA = 0.20
[Formula]
CPU_WEIGHT = 0.55
URGENCY_WEIGHT = 0.20
CHEAP_NOW_WEIGHT = 0.15
LOOKAHEAD_WEIGHT = 0.10
THERMAL_WEIGHT = 0.25
[Thermal]
ENABLED = true
WARM_TEMP = 70
HOT_TEMP = 80
CRITICAL_TEMP = 90
[Transition]
MIN_HOLD_SEC = 60
UPSCALE_COOLDOWN_SEC = 15
SCORE_MARGIN = 7.5Swedish electricity price area used when fetching prices.
[AreaCode]
AREA = SE3Common Swedish areas are:
SE1
SE2
SE3
SE4
Controls the main daemon loop and runtime behaviour.
[Runtime]
SLEEP_INTERVAL = 5
COMMIT_INTERVAL_MINUTES = 5
USAGE_METHOD = median
DRY_RUN = falseSLEEP_INTERVAL: how often the main loop runs, in seconds.COMMIT_INTERVAL_MINUTES: how often buffered CPU samples are committed to SQLite.USAGE_METHOD: how per-core CPU usage is aggregated for runtime decisions.DRY_RUN: calculates decisions without writing to governor files.
Supported usage methods:
median
average
max
median is usually less jumpy. average is more sensitive to broad load across cores. max reacts more strongly to the busiest core.
[Storage]
DATABASE_DIR = .Directory where prices.db and cpu.db are stored. Relative paths are resolved from the script directory.
[Logging]
LEVEL = INFOCommon values:
DEBUG
INFO
WARNING
ERROR
Controls automatic cleanup of old database rows.
[DataRetention]
ENABLED = true
DAYS = 30[Network]
REQUEST_TIMEOUT_SEC = 10Timeout for electricity price API requests.
Controls CPU smoothing and workload classification thresholds.
[Metrics]
SMOOTH_FACTOR = 0.30
HISTORY_SAMPLES = 24
HIGH_LOAD_THRESHOLD = 70
BURST_CPU_THRESHOLD = 75
IDLE_CPU_THRESHOLD = 15
IOWAIT_THRESHOLD = 25SMOOTH_FACTOR: exponential moving average factor. Must be greater than0and less than or equal to1.HISTORY_SAMPLES: number of smoothed samples used for sustained-load detection.HIGH_LOAD_THRESHOLD: CPU percentage considered high for sustained-load detection.BURST_CPU_THRESHOLD: CPU percentage that can classify a short load as burst.IDLE_CPU_THRESHOLD: CPU percentage considered idle when load average is also low.IOWAIT_THRESHOLD: I/O wait percentage that can classify the workload as I/O-bound.
Controls daily price rank classification and lookahead behaviour.
[PriceStrategy]
CHEAP_RANK_MAX = 0.25
EXPENSIVE_RANK_MIN = 0.80
LOOKAHEAD_HOURS = 3
LOOKAHEAD_DELTA = 0.20PowerNap compares the current price against the day’s price range:
0.0 = cheapest point in the day
1.0 = most expensive point in the day
CHEAP_RANK_MAX: rank at or below this is treated as cheap.EXPENSIVE_RANK_MIN: rank at or above this is treated as expensive.LOOKAHEAD_HOURS: number of future hours used for price lookahead.LOOKAHEAD_DELTA: rank difference considered meaningful for rising/falling price trends.
Controls the formulaic aggression score.
[Formula]
CPU_WEIGHT = 0.55
URGENCY_WEIGHT = 0.20
CHEAP_NOW_WEIGHT = 0.15
LOOKAHEAD_WEIGHT = 0.10
THERMAL_WEIGHT = 0.25The score is calculated as:
score =
load_score * CPU_WEIGHT
+ urgency_score * URGENCY_WEIGHT
+ cheap_now_score * CHEAP_NOW_WEIGHT
+ lookahead_score * LOOKAHEAD_WEIGHT
- thermal_penalty * THERMAL_WEIGHT
The score does not blindly decide everything. It adjusts the policy baseline while safety overrides and transition rules still apply.
Controls thermal safety behaviour.
[Thermal]
ENABLED = true
WARM_TEMP = 70
HOT_TEMP = 80
CRITICAL_TEMP = 90WARM_TEMP: begins reducing aggressiveness.HOT_TEMP: reduces aggressiveness more strongly.CRITICAL_TEMP: overrides everything and forcespowersave.
Controls stability around governor changes.
[Transition]
MIN_HOLD_SEC = 60
UPSCALE_COOLDOWN_SEC = 15
SCORE_MARGIN = 7.5MIN_HOLD_SEC: minimum time before allowing a normal downshift.UPSCALE_COOLDOWN_SEC: minimum time after an upscale before allowing a downscale.SCORE_MARGIN: minimum score movement required before accepting a governor change.
PowerNap uses a layered decision model:
1. Safety overrides
2. Metrics collection
3. Workload classification
4. Price classification
5. Policy matrix baseline
6. Formulaic aggression adjustment
7. Transition gating
idle
light
balanced
burst
sustained_heavy
io_bound
cheap
normal
expensive
unknown
normal
warm
hot
critical
unknown
cheap normal expensive unknown
idle powersave powersave powersave powersave
light conservative powersave powersave powersave
balanced schedutil conservative powersave conservative
burst schedutil schedutil conservative conservative
sustained_heavy performance schedutil schedutil schedutil
io_bound conservative conservative powersave conservative
The formula score can adjust the baseline by one level, but critical thermal state overrides everything.
PowerNap chooses between these governors when available:
powersave
conservative
schedutil
performance
The rough behaviour is:
- Idle or light load tends toward
powersave. - Balanced load can use
conservativeorschedutildepending on price rank. - Burst load can use
schedutilwhen price conditions are not hostile. - Sustained heavy load can use
performancewhen electricity is cheap. - Expensive power suppresses non-urgent workloads.
- High temperature reduces aggressiveness.
- Critical temperature forces
powersave.
If a chosen governor is not supported by the system, PowerNap falls back to the closest supported governor based on governor priority.
Run the daemon:
python3 powernap.pyRun in dry-run mode:
python3 powernap.py --dry-runGenerate a PrettyTable report:
python3 powernap.py --reportShow raw diagnostic output:
python3 powernap.py --debugValidate configuration and environment:
python3 powernap.py --check-configUse a specific config file:
python3 powernap.py --config /path/to/powernap.confPrettyTable is used by the explicit report command:
python3 powernap.py --reportThe report reads SQLite history and prints presentation-friendly tables for:
latest prices
governor events
CPU usage samples
This is a separate process from the daemon. It does not run as a thread inside the daemon.
An example powernap.service is included.
Recommended install layout:
sudo mkdir -p /opt/powernap
sudo cp powernap.py powernap.conf.example requirements.txt /opt/powernap/
sudo cp /opt/powernap/powernap.conf.example /opt/powernap/powernap.conf
sudo chmod 700 /opt/powernap
sudo chmod 600 /opt/powernap/powernap.conf
sudo chmod 755 /opt/powernap/powernap.pyInstall the service:
sudo cp powernap.service /etc/systemd/system/powernap.service
sudo systemctl daemon-reload
sudo systemctl enable --now powernap.serviceCheck logs:
journalctl -u powernap.service -fExample service:
[Unit]
Description=PowerNap formulaic CPU governor controller
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
WorkingDirectory=/opt/powernap
Environment=POWERNAP_CONFIG=/opt/powernap/powernap.conf
ExecStart=/usr/bin/python3 /opt/powernap/powernap.py
Restart=on-failure
RestartSec=5
NoNewPrivileges=true
PrivateTmp=true
ProtectHome=true
ProtectSystem=full
ReadWritePaths=/opt/powernap /sys/devices/system/cpu
[Install]
WantedBy=multi-user.target
PowerNap needs permission to write to files like:
/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor
Depending on your distro and kernel, this may require one of these approaches:
- Run the service as root.
- Use a tightly scoped helper for governor writes.
- Use a carefully limited sudoers rule.
- Adjust udev/system permissions if appropriate for your system.
Avoid giving broad root access if you do not need it.
-
Keep
powernap.confowner-readable only:chmod 600 powernap.conf
-
Keep database files private if they are stored in a shared location.
-
Run as a dedicated service user where possible.
-
Use systemd hardening options where possible.
-
Avoid storing PowerNap in world-writable directories.
-
Do not expose PowerNap files or SQLite databases through a web server.
Show raw database contents:
python3 powernap.py --debugShow presentation output:
python3 powernap.py --reportCheck supported governors manually:
cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_available_governorsCheck current governor:
cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governorCheck service logs:
journalctl -u powernap.service -n 100 --no-pagerYour CPU, kernel, VM, container, or power driver may not expose CPU frequency governor control.
Check:
ls /sys/devices/system/cpu/cpu*/cpufreq/scaling_governorThe service user does not have permission to write to scaling_governor.
Check service logs:
journalctl -u powernap.service -fPowerNap will try to fetch price data for the current date. If the API is unavailable, it backs off and retries later.
Check logs for API errors:
journalctl -u powernap.service -fIncrease transition stability:
[Transition]
MIN_HOLD_SEC = 120
UPSCALE_COOLDOWN_SEC = 30
SCORE_MARGIN = 10Increase smoothing:
[Metrics]
SMOOTH_FACTOR = 0.20Reduce transition stability:
[Transition]
MIN_HOLD_SEC = 30
UPSCALE_COOLDOWN_SEC = 10
SCORE_MARGIN = 5Increase smoothing responsiveness:
[Metrics]
SMOOTH_FACTOR = 0.40The daemon may not have committed history yet, or it may be using a different database directory.
Check:
python3 powernap.py --check-configAlso confirm:
[Storage]
DATABASE_DIR = .This project is licensed under the GPL-3.0 License.