Skip to content

Instantly share code, notes, and snippets.

@cbrunnkvist
Created August 13, 2025 12:51
Show Gist options
  • Save cbrunnkvist/6ad2f44787048de268281a0e01dcf621 to your computer and use it in GitHub Desktop.
Save cbrunnkvist/6ad2f44787048de268281a0e01dcf621 to your computer and use it in GitHub Desktop.
Laptop battery as virtual NUT UPS (via dummy-ups driver)

NUT Configuration for Laptop Battery Virtual UPS

1. /etc/nut/ups.conf

Add this section to your ups.conf:

[laptop-battery]
    driver = dummy-ups
    port = /var/lib/nut/laptop-battery.dev
    desc = "ThinkPad Battery Virtual UPS"
    # Optional: set polling interval
    # pollinterval = 30

2. /etc/nut/upsd.conf

Basic configuration (modify as needed):

# Network UPS Tools upsd configuration file

# LISTEN <address> [<port>]
LISTEN 127.0.0.1 3493
LISTEN ::1 3493

# Maximum number of connections
MAXCONN 1024

# Certificate and key files for SSL
# CERTFILE /etc/nut/upsd.pem
# KEYFILE /etc/nut/upsd.key

3. /etc/nut/upsd.users

Create users for accessing the UPS:

# Network UPS Tools upsd users file

[admin]
    password = your_admin_password
    actions = SET
    instcmds = ALL
    upsmon master

[upsmon]
    password = your_monitor_password
    upsmon master

4. /etc/nut/upsmon.conf

Configure the monitoring daemon:

# Network UPS Tools upsmon configuration

# Monitor the laptop battery UPS
MONITOR laptop-battery@localhost 1 upsmon your_monitor_password master

# Shutdown command
SHUTDOWNCMD "/sbin/shutdown -h +0"

# Notification command (optional)
NOTIFYCMD /usr/sbin/upssched

# How long to wait before declaring UPS dead
DEADTIME 15

# Power values
MINSUPPLIES 1

# Notification flags
NOTIFYFLAG ONBATT  SYSLOG+WALL+EXEC
NOTIFYFLAG LOWBATT SYSLOG+WALL+EXEC
NOTIFYFLAG ONLINE  SYSLOG+WALL+EXEC
NOTIFYFLAG REPLBATT SYSLOG+WALL
NOTIFYFLAG BADCOMM SYSLOG+WALL
NOTIFYFLAG COMMOK  SYSLOG+WALL
NOTIFYFLAG NOCOMM  SYSLOG+WALL+EXEC

# Notification messages
NOTIFYMSG ONLINE    "UPS %s: On line power"
NOTIFYMSG ONBATT    "UPS %s: On battery"
NOTIFYMSG LOWBATT   "UPS %s: Battery is low"
NOTIFYMSG REPLBATT  "UPS %s: Battery needs to be replaced"
NOTIFYMSG BADCOMM   "UPS %s: Communication failure"
NOTIFYMSG COMMOK    "UPS %s: Communication restored"
NOTIFYMSG NOCOMM    "UPS %s: Communication lost"

# Run as user
RUN_AS_USER nut

# Poll frequency
POLLFREQ 5
POLLFREQALERT 5

# Hostname for notifications
# HOSTSYNC 15

5. /etc/nut/nut.conf

Set the NUT mode:

# Network UPS Tools configuration

# Mode can be: none, standalone, netserver, netclient
MODE=standalone

6. Systemd Timer for Battery Updates

Create /etc/systemd/system/laptop-battery-nut.service:

[Unit]
Description=Update Laptop Battery Status for NUT
After=multi-user.target

[Service]
Type=oneshot
User=nut
ExecStart=/usr/local/bin/laptop-battery-to-nut.sh

Create /etc/systemd/system/laptop-battery-nut.timer:

[Unit]
Description=Update Laptop Battery Status for NUT every 30 seconds
Requires=laptop-battery-nut.service

[Timer]
OnCalendar=*:*:0/30
Persistent=true

[Install]
WantedBy=timers.target

Setup Instructions

  1. Install the script:

    sudo cp laptop-battery-to-nut.sh /usr/local/bin/
    sudo chmod +x /usr/local/bin/laptop-battery-to-nut.sh
    sudo chown nut:nut /usr/local/bin/laptop-battery-to-nut.sh
  2. Create initial battery data file:

    # Ensure the nut user owns the data directory
    sudo mkdir -p /var/lib/nut
    sudo chown nut:nut /var/lib/nut
    sudo chmod 755 /var/lib/nut
    
    # Run the script to create initial data
    sudo -u nut /usr/local/bin/laptop-battery-to-nut.sh
  3. Set up systemd timer:

    sudo systemctl daemon-reload
    sudo systemctl enable laptop-battery-nut.timer
    sudo systemctl start laptop-battery-nut.timer
  4. Start NUT services:

    sudo systemctl enable [email protected]
    sudo systemctl start [email protected]
    sudo systemctl enable nut-server.service
    sudo systemctl start nut-server.service
    sudo systemctl enable nut-monitor.service
    sudo systemctl start nut-monitor.service
  5. Test the setup:

    upsc laptop-battery
    upsc laptop-battery ups.status
    upsc laptop-battery battery.charge

Troubleshooting

  • Check logs: journalctl -u [email protected]
  • Test script: sudo -u nut /usr/local/bin/laptop-battery-to-nut.sh
  • Verify file: cat /var/lib/nut/laptop-battery.dev
  • Check permissions: Ensure the nut user can read battery information

Customization

You can modify the script to:

  • Adjust the low battery threshold
  • Add temperature monitoring if available
  • Customize UPS model/manufacturer names
  • Add additional battery metrics
  • Support multiple batteries
#!/bin/bash
# laptop-battery-to-nut.sh - Convert ThinkPad battery/power status to NUT dummy-ups format
# Usage: Run this script periodically (e.g., via cron every 30 seconds)
# Configuration
OUTPUT_DIR="/var/lib/nut"
OUTPUT_FILE="$OUTPUT_DIR/laptop-battery.dev"
TEMP_FILE="/tmp/laptop-battery.dev.tmp"
# Create output directory if it doesn't exist
if [ ! -d "$OUTPUT_DIR" ]; then
mkdir -p "$OUTPUT_DIR" 2>/dev/null || {
echo "Error: Cannot create $OUTPUT_DIR. Run as root or ensure nut user has access." >&2
exit 1
}
fi
BATTERY_PATH="/sys/class/power_supply/BAT0"
AC_ADAPTER_PATH="/sys/class/power_supply/ADP1"
# Alternative paths for different systems
if [ ! -d "$BATTERY_PATH" ]; then
BATTERY_PATH="/sys/class/power_supply/BAT1"
fi
if [ ! -d "$AC_ADAPTER_PATH" ]; then
AC_ADAPTER_PATH="/sys/class/power_supply/AC"
fi
if [ ! -d "$AC_ADAPTER_PATH" ]; then
AC_ADAPTER_PATH="/sys/class/power_supply/ACAD"
fi
# Check if battery exists
if [ ! -d "$BATTERY_PATH" ]; then
echo "Error: No battery found in /sys/class/power_supply/" >&2
exit 1
fi
# Function to read file with fallback
read_battery_value() {
local file="$1"
local default="$2"
if [ -r "$BATTERY_PATH/$file" ]; then
cat "$BATTERY_PATH/$file" 2>/dev/null || echo "$default"
else
echo "$default"
fi
}
read_ac_value() {
local file="$1"
local default="$2"
if [ -r "$AC_ADAPTER_PATH/$file" ]; then
cat "$AC_ADAPTER_PATH/$file" 2>/dev/null || echo "$default"
else
echo "$default"
fi
}
# Read battery information
BATTERY_STATUS=$(read_battery_value "status" "Unknown")
BATTERY_CAPACITY=$(read_battery_value "capacity" "0")
BATTERY_VOLTAGE=$(read_battery_value "voltage_now" "0")
BATTERY_CURRENT=$(read_battery_value "current_now" "0")
BATTERY_ENERGY_NOW=$(read_battery_value "energy_now" "0")
BATTERY_ENERGY_FULL=$(read_battery_value "energy_full" "0")
BATTERY_POWER_NOW=$(read_battery_value "power_now" "0")
BATTERY_CYCLE_COUNT=$(read_battery_value "cycle_count" "0")
BATTERY_HEALTH=$(read_battery_value "health" "Good")
# Read AC adapter information
AC_ONLINE=$(read_ac_value "online" "0")
# Convert values to appropriate units
# Voltage from microvolts to volts
BATTERY_VOLTAGE_V=$(echo "scale=1; $BATTERY_VOLTAGE / 1000000" | bc 2>/dev/null || echo "0.0")
# Power from microwatts to watts
BATTERY_POWER_W=$(echo "scale=1; $BATTERY_POWER_NOW / 1000000" | bc 2>/dev/null || echo "0.0")
# Calculate runtime (very rough estimate)
if [ "$BATTERY_POWER_NOW" -gt 0 ] && [ "$BATTERY_STATUS" = "Discharging" ]; then
# Runtime in seconds = (energy_now / power_now) * 3600
RUNTIME=$(echo "scale=0; ($BATTERY_ENERGY_NOW * 3600) / $BATTERY_POWER_NOW" | bc 2>/dev/null || echo "0")
else
RUNTIME="0"
fi
# Determine UPS status
UPS_STATUS=""
case "$BATTERY_STATUS" in
"Charging")
if [ "$AC_ONLINE" = "1" ]; then
UPS_STATUS="OL CHRG" # Online, Charging
else
UPS_STATUS="OB CHRG" # On Battery, Charging (shouldn't happen normally)
fi
;;
"Discharging")
UPS_STATUS="OB DISCHRG" # On Battery, Discharging
if [ "$BATTERY_CAPACITY" -le 15 ]; then
UPS_STATUS="$UPS_STATUS LB" # Add Low Battery
fi
;;
"Not charging"|"Full")
if [ "$AC_ONLINE" = "1" ]; then
UPS_STATUS="OL" # Online
else
UPS_STATUS="OB" # On Battery
fi
;;
*)
if [ "$AC_ONLINE" = "1" ]; then
UPS_STATUS="OL" # Online
else
UPS_STATUS="OB" # On Battery
fi
;;
esac
# Generate the .dev file for dummy-ups
cat > "$TEMP_FILE" << EOF
# Laptop Battery Virtual UPS - Generated $(date)
battery.charge: $BATTERY_CAPACITY
battery.charge.low: 15
battery.charge.warning: 25
battery.runtime: $RUNTIME
battery.type: "Li-ion"
battery.voltage: $BATTERY_VOLTAGE_V
battery.voltage.nominal: 11.1
device.mfr: "ThinkPad"
device.model: "Virtual Battery UPS"
device.serial: "LAPTOP001"
device.type: "ups"
driver.name: "dummy-ups"
driver.version: "2.8.0"
driver.version.data: "Laptop Battery"
input.voltage: 120.0
input.voltage.nominal: 120.0
output.voltage: 120.0
output.voltage.nominal: 120.0
ups.beeper.status: "disabled"
ups.delay.shutdown: 20
ups.delay.start: 30
ups.firmware: "Virtual"
ups.load: $(echo "scale=0; $BATTERY_POWER_W / 0.65" | bc 2>/dev/null || echo "0")
ups.mfr: "ThinkPad"
ups.model: "Virtual Battery UPS"
ups.power.nominal: 65
ups.productid: "0001"
ups.serial: "LAPTOP001"
ups.status: $UPS_STATUS
ups.test.result: "No test initiated"
ups.timer.reboot: 0
ups.timer.shutdown: -1
ups.vendorid: "17ef"
EOF
# Atomically move the temporary file to the final location
if mv "$TEMP_FILE" "$OUTPUT_FILE"; then
echo "Updated laptop battery status: $UPS_STATUS (${BATTERY_CAPACITY}%)"
else
echo "Error: Failed to update $OUTPUT_FILE" >&2
exit 1
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment