mirror of
https://github.com/actuallymentor/battery.git
synced 2026-02-03 14:03:24 +00:00
1043 lines
28 KiB
Bash
Executable File
1043 lines
28 KiB
Bash
Executable File
#!/bin/bash
|
||
|
||
## ###############
|
||
## Update management
|
||
## variables are used by this binary as well at the update script
|
||
## ###############
|
||
BATTERY_CLI_VERSION="v1.2.7"
|
||
|
||
# Path fixes for unexpected environments
|
||
PATH=/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
|
||
|
||
## ###############
|
||
## Variables
|
||
## ###############
|
||
binfolder=/usr/local/bin
|
||
visudo_folder=/private/etc/sudoers.d
|
||
visudo_file=${visudo_folder}/battery
|
||
configfolder=$HOME/.battery
|
||
pidfile=$configfolder/battery.pid
|
||
logfile=$configfolder/battery.log
|
||
maintain_percentage_tracker_file=$configfolder/maintain.percentage
|
||
maintain_voltage_tracker_file=$configfolder/maintain.voltage
|
||
daemon_path=$HOME/Library/LaunchAgents/battery.plist
|
||
calibrate_pidfile=$configfolder/calibrate.pid
|
||
smc_capabilities_detected=false
|
||
smc_supports_tahoe=false
|
||
smc_supports_legacy=false
|
||
smc_supports_adapter_chie=false
|
||
smc_supports_adapter_ch0i=false
|
||
smc_supports_adapter_ch0j=false
|
||
|
||
# Voltage limits
|
||
voltage_min="10.5"
|
||
voltage_max="12.6"
|
||
voltage_hyst_min="0.1"
|
||
voltage_hyst_max="2"
|
||
|
||
## ###############
|
||
## Housekeeping
|
||
## ###############
|
||
|
||
# Create config folder if needed
|
||
mkdir -p $configfolder
|
||
|
||
# create logfile if needed
|
||
touch $logfile
|
||
|
||
# Trim logfile if needed
|
||
logsize=$(stat -f%z "$logfile")
|
||
max_logsize_bytes=5000000
|
||
if ((logsize > max_logsize_bytes)); then
|
||
tail -n 100 $logfile >$logfile
|
||
fi
|
||
|
||
# CLI help message
|
||
helpmessage="
|
||
Battery CLI utility $BATTERY_CLI_VERSION
|
||
|
||
Usage:
|
||
|
||
battery status
|
||
output battery SMC status, % and time remaining
|
||
|
||
battery logs LINES[integer, optional]
|
||
output logs of the battery CLI and GUI
|
||
eg: battery logs 100
|
||
|
||
battery maintain PERCENTAGE[1-100,stop]
|
||
reboot-persistent battery level maintenance: turn off charging above, and on below a certain value
|
||
it has the option of a --force-discharge flag that discharges even when plugged in (this does NOT work well with clamshell mode)
|
||
eg: battery maintain 80
|
||
eg: battery maintain stop
|
||
|
||
battery maintain VOLTAGE[${voltage_min}V-${voltage_max}V,stop] (HYSTERESIS[${voltage_hyst_min}V-${voltage_hyst_max}V])
|
||
reboot-persistent battery level maintenance: keep battery at a certain voltage
|
||
default hysteresis: 0.1V
|
||
eg: battery maintain 11.4V # keeps battery between 11.3V and 11.5V
|
||
eg: battery maintain 11.4V 0.3V # keeps battery between 11.1V and 11.7V
|
||
|
||
battery charging SETTING[on/off]
|
||
manually set the battery to (not) charge
|
||
eg: battery charging on
|
||
|
||
battery adapter SETTING[on/off]
|
||
manually set the adapter to (not) charge even when plugged in
|
||
eg: battery adapter off
|
||
|
||
battery calibrate
|
||
calibrate the battery by discharging it to 15%, then recharging it to 100%, and keeping it there for 1 hour
|
||
|
||
battery charge LEVEL[1-100]
|
||
charge the battery to a certain percentage, and disable charging when that percentage is reached
|
||
eg: battery charge 90
|
||
|
||
battery discharge LEVEL[1-100]
|
||
block power input from the adapter until battery falls to this level
|
||
eg: battery discharge 90
|
||
|
||
battery visudo
|
||
ensure you don't need to call battery with sudo
|
||
This is already used in the setup script, so you should't need it.
|
||
|
||
battery update
|
||
update the battery utility to the latest version
|
||
|
||
battery reinstall
|
||
reinstall the battery utility to the latest version (reruns the installation script)
|
||
|
||
battery uninstall
|
||
enable charging, remove the smc tool, and the battery script
|
||
|
||
"
|
||
|
||
# Visudo instructions
|
||
visudoconfig="
|
||
# Visudo settings for the battery utility installed from https://github.com/actuallymentor/battery
|
||
# intended to be placed in $visudo_file on a mac
|
||
Cmnd_Alias BATTERYOFF = $binfolder/smc -k CH0B -w 02, $binfolder/smc -k CH0C -w 02, $binfolder/smc -k CH0B -r, $binfolder/smc -k CH0C -r
|
||
Cmnd_Alias BATTERYON = $binfolder/smc -k CH0B -w 00, $binfolder/smc -k CH0C -w 00
|
||
Cmnd_Alias DISCHARGEOFF = $binfolder/smc -k CH0I -w 00, $binfolder/smc -k CH0I -r
|
||
Cmnd_Alias DISCHARGEON = $binfolder/smc -k CH0I -w 01
|
||
Cmnd_Alias LEDCONTROL = $binfolder/smc -k ACLC -w 04, $binfolder/smc -k ACLC -w 03, $binfolder/smc -k ACLC -w 02, $binfolder/smc -k ACLC -w 01, $binfolder/smc -k ACLC -w 00, $binfolder/smc -k ACLC -r
|
||
Cmnd_Alias CHTE = $binfolder/smc -k CHTE -r, $binfolder/smc -k CHTE -w 00000000, $binfolder/smc -k CHTE -w 01000000
|
||
Cmnd_Alias CHIE = $binfolder/smc -k CHIE -r, $binfolder/smc -k CHIE -w 08, $binfolder/smc -k CHIE -w 00
|
||
Cmnd_Alias CH0J = $binfolder/smc -k CH0J -r, $binfolder/smc -k CH0J -w 01, $binfolder/smc -k CH0J -w 00
|
||
ALL ALL = NOPASSWD: BATTERYOFF
|
||
ALL ALL = NOPASSWD: BATTERYON
|
||
ALL ALL = NOPASSWD: DISCHARGEOFF
|
||
ALL ALL = NOPASSWD: DISCHARGEON
|
||
ALL ALL = NOPASSWD: LEDCONTROL
|
||
ALL ALL = NOPASSWD: CHTE
|
||
ALL ALL = NOPASSWD: CHIE
|
||
ALL ALL = NOPASSWD: CH0J
|
||
"
|
||
|
||
# Get parameters
|
||
battery_binary=$0
|
||
action=$1
|
||
setting=$2
|
||
subsetting=$3
|
||
|
||
## ###############
|
||
## Helpers
|
||
## ###############
|
||
|
||
function log() {
|
||
echo -e "$(date +%D-%T) - $1"
|
||
}
|
||
|
||
function valid_percentage() {
|
||
if ! [[ "$1" =~ ^[0-9]+$ ]] || [[ "$1" -lt 0 ]] || [[ "$1" -gt 100 ]]; then
|
||
return 1
|
||
else
|
||
return 0
|
||
fi
|
||
}
|
||
|
||
function valid_voltage() {
|
||
if [[ "$1" =~ ^[0-9]+(\.[0-9]+)?V$ ]]; then
|
||
return 0
|
||
fi
|
||
return 1
|
||
}
|
||
|
||
function detect_smc_capabilities() {
|
||
if [[ "$smc_capabilities_detected" == "true" ]]; then
|
||
return
|
||
fi
|
||
|
||
smc_capabilities_detected=true
|
||
smc_supports_tahoe=false
|
||
smc_supports_legacy=false
|
||
smc_supports_adapter_chie=false
|
||
smc_supports_adapter_ch0i=false
|
||
smc_supports_adapter_ch0j=false
|
||
|
||
if sudo -n smc -k CHTE -r &>/dev/null; then
|
||
smc_supports_tahoe=true
|
||
fi
|
||
if sudo -n smc -k CH0B -r &>/dev/null && sudo -n smc -k CH0C -r &>/dev/null; then
|
||
smc_supports_legacy=true
|
||
fi
|
||
if sudo -n smc -k CHIE -r &>/dev/null; then
|
||
smc_supports_adapter_chie=true
|
||
fi
|
||
if sudo -n smc -k CH0I -r &>/dev/null; then
|
||
smc_supports_adapter_ch0i=true
|
||
fi
|
||
if sudo -n smc -k CH0J -r &>/dev/null; then
|
||
smc_supports_adapter_ch0j=true
|
||
fi
|
||
|
||
log "SMC capabilities: tahoe=$smc_supports_tahoe legacy=$smc_supports_legacy CHIE=$smc_supports_adapter_chie CH0I=$smc_supports_adapter_ch0i CH0J=$smc_supports_adapter_ch0j"
|
||
}
|
||
|
||
function smc_read_hex() {
|
||
local key=$1
|
||
local value=$(sudo -n smc -k "$key" -r 2>/dev/null | awk '{print $4}')
|
||
value=${value//:/}
|
||
echo "$value"
|
||
}
|
||
|
||
function smc_write_hex() {
|
||
local key=$1
|
||
local hex_value=$2
|
||
if ! sudo smc -k "$key" -w "$hex_value" >/dev/null 2>&1; then
|
||
log "⚠️ Failed to write $hex_value to $key"
|
||
return 1
|
||
fi
|
||
return 0
|
||
}
|
||
|
||
## #################
|
||
## SMC Manipulation
|
||
## #################
|
||
|
||
# Change magsafe color
|
||
# see community sleuthing: https://github.com/actuallymentor/battery/issues/71
|
||
function change_magsafe_led_color() {
|
||
log "MagSafe LED function invoked"
|
||
color=$1
|
||
|
||
# Check whether user can run color changes without password (required for backwards compatibility)
|
||
if sudo -n smc -k ACLC -r &>/dev/null; then
|
||
log "💡 Setting magsafe color to $color"
|
||
else
|
||
log "🚨 Your version of battery is using an old visudo file, please run 'battery visudo' to fix this, until you do battery cannot change magsafe led colors"
|
||
return
|
||
fi
|
||
|
||
if [[ "$color" == "green" ]]; then
|
||
log "setting LED to green"
|
||
sudo smc -k ACLC -w 03
|
||
elif [[ "$color" == "orange" ]]; then
|
||
log "setting LED to orange"
|
||
sudo smc -k ACLC -w 04
|
||
else
|
||
# Default action: reset. Value 00 is a guess and needs confirmation
|
||
log "resetting LED"
|
||
sudo smc -k ACLC -w 00
|
||
fi
|
||
}
|
||
|
||
# Re:discharging, we're using keys uncovered by @howie65: https://github.com/actuallymentor/battery/issues/20#issuecomment-1364540704
|
||
# CH0I seems to be the "disable the adapter" key
|
||
function enable_discharging() {
|
||
log "🔽🪫 Enabling battery discharging"
|
||
detect_smc_capabilities
|
||
if [[ "$smc_supports_adapter_chie" == "true" ]]; then
|
||
smc_write_hex CHIE 08
|
||
elif [[ "$smc_supports_adapter_ch0j" == "true" ]]; then
|
||
smc_write_hex CH0J 01
|
||
else
|
||
smc_write_hex CH0I 01
|
||
fi
|
||
sudo smc -k ACLC -w 01
|
||
}
|
||
|
||
function disable_discharging() {
|
||
log "🔼🪫 Disabling battery discharging"
|
||
detect_smc_capabilities
|
||
if [[ "$smc_supports_adapter_chie" == "true" ]]; then
|
||
smc_write_hex CHIE 00
|
||
elif [[ "$smc_supports_adapter_ch0j" == "true" ]]; then
|
||
smc_write_hex CH0J 00
|
||
elif [[ "$smc_supports_adapter_ch0i" == "true" ]]; then
|
||
smc_write_hex CH0I 00
|
||
else
|
||
smc_write_hex CH0I 00
|
||
fi
|
||
# Keep track of status
|
||
is_charging=$(get_smc_charging_status)
|
||
|
||
if ! valid_percentage "$setting"; then
|
||
|
||
log "Disabling discharging: No valid maintain percentage set, enabling charging"
|
||
# use direct commands since enable_charging also calls disable_discharging, and causes an eternal loop
|
||
if [[ "$smc_supports_tahoe" == "true" ]]; then
|
||
smc_write_hex CHTE 00000000
|
||
elif [[ "$smc_supports_legacy" == "true" ]]; then
|
||
smc_write_hex CH0B 00
|
||
smc_write_hex CH0C 00
|
||
else
|
||
log "⚠️ Unable to reset charging state"
|
||
fi
|
||
change_magsafe_led_color "orange"
|
||
|
||
elif [[ "$battery_percentage" -ge "$setting" && "$is_charging" == "enabled" ]]; then
|
||
|
||
log "Disabling discharging: Charge above $setting, disabling charging"
|
||
disable_charging
|
||
change_magsafe_led_color "green"
|
||
|
||
elif [[ "$battery_percentage" -lt "$setting" && "$is_charging" == "disabled" ]]; then
|
||
|
||
log "Disabling discharging: Charge below $setting, enabling charging"
|
||
# use direct commands since enable_charging also calls disable_discharging, and causes an eternal loop
|
||
if [[ "$smc_supports_tahoe" == "true" ]]; then
|
||
smc_write_hex CHTE 00000000
|
||
elif [[ "$smc_supports_legacy" == "true" ]]; then
|
||
smc_write_hex CH0B 00
|
||
smc_write_hex CH0C 00
|
||
else
|
||
log "⚠️ Unable to reset charging state"
|
||
fi
|
||
change_magsafe_led_color "orange"
|
||
|
||
fi
|
||
|
||
battery_percentage=$(get_battery_percentage)
|
||
}
|
||
|
||
# Re:charging, Aldente uses CH0B https://github.com/davidwernhart/AlDente/blob/0abfeafbd2232d16116c0fe5a6fbd0acb6f9826b/AlDente/Helper.swift#L227
|
||
# but @joelucid uses CH0C https://github.com/davidwernhart/AlDente/issues/52#issuecomment-1019933570
|
||
# so I'm using both since with only CH0B I noticed sometimes during sleep it does trigger charging
|
||
function enable_charging() {
|
||
log "🔌🔋 Enabling battery charging"
|
||
detect_smc_capabilities
|
||
if [[ "$smc_supports_tahoe" == "true" ]]; then
|
||
smc_write_hex CHTE 00000000
|
||
elif [[ "$smc_supports_legacy" == "true" ]]; then
|
||
smc_write_hex CH0B 00
|
||
smc_write_hex CH0C 00
|
||
else
|
||
log "⚠️ Unable to determine SMC keys for enabling charging"
|
||
fi
|
||
disable_discharging
|
||
}
|
||
|
||
function disable_charging() {
|
||
log "🔌🪫 Disabling battery charging"
|
||
detect_smc_capabilities
|
||
if [[ "$smc_supports_tahoe" == "true" ]]; then
|
||
smc_write_hex CHTE 01000000
|
||
elif [[ "$smc_supports_legacy" == "true" ]]; then
|
||
smc_write_hex CH0B 02
|
||
smc_write_hex CH0C 02
|
||
else
|
||
log "⚠️ Unable to determine SMC keys for disabling charging"
|
||
fi
|
||
}
|
||
|
||
function get_smc_charging_status() {
|
||
detect_smc_capabilities
|
||
local status_key="CH0B"
|
||
if [[ "$smc_supports_tahoe" == "true" ]]; then
|
||
status_key="CHTE"
|
||
fi
|
||
hex_status=$(smc_read_hex "$status_key")
|
||
if [[ -z "$hex_status" ]]; then
|
||
echo "unknown"
|
||
return
|
||
fi
|
||
if [[ "$smc_supports_tahoe" == "true" ]]; then
|
||
if [[ "$hex_status" == "00000000" ]]; then
|
||
echo "enabled"
|
||
else
|
||
echo "disabled"
|
||
fi
|
||
elif [[ "$hex_status" == "00" ]]; then
|
||
echo "enabled"
|
||
else
|
||
echo "disabled"
|
||
fi
|
||
}
|
||
|
||
function get_smc_discharging_status() {
|
||
detect_smc_capabilities
|
||
local status_key="CH0I"
|
||
if [[ "$smc_supports_adapter_chie" == "true" ]]; then
|
||
status_key="CHIE"
|
||
elif [[ "$smc_supports_adapter_ch0j" == "true" ]]; then
|
||
status_key="CH0J"
|
||
fi
|
||
hex_status=$(smc_read_hex "$status_key")
|
||
if [[ -z "$hex_status" ]]; then
|
||
echo "unknown"
|
||
return
|
||
fi
|
||
if [[ "$hex_status" == "0" || "$hex_status" == "00" ]]; then
|
||
echo "not discharging"
|
||
else
|
||
echo "discharging"
|
||
fi
|
||
}
|
||
|
||
## ###############
|
||
## Statistics
|
||
## ###############
|
||
|
||
function get_battery_percentage() {
|
||
battery_percentage=$(pmset -g batt | tail -n1 | awk '{print $3}' | sed s:\%\;::)
|
||
echo "$battery_percentage"
|
||
}
|
||
|
||
function get_remaining_time() {
|
||
time_remaining=$(pmset -g batt | tail -n1 | awk '{print $5}')
|
||
echo "$time_remaining"
|
||
}
|
||
|
||
function get_charger_state() {
|
||
ac_attached=$(pmset -g batt | tail -n1 | awk '{ x=match($0, /AC attached/) > 0; print x }')
|
||
echo "$ac_attached"
|
||
}
|
||
|
||
function get_maintain_percentage() {
|
||
maintain_percentage=$(cat $maintain_percentage_tracker_file 2>/dev/null)
|
||
echo "$maintain_percentage"
|
||
}
|
||
|
||
function get_voltage() {
|
||
voltage=$(ioreg -l -n AppleSmartBattery -r | grep "\"Voltage\" =" | awk '{ print $3/1000 }' | tr ',' '.')
|
||
echo "$voltage"
|
||
}
|
||
|
||
## ###############
|
||
## Actions
|
||
## ###############
|
||
|
||
# Help message
|
||
if [ -z "$action" ] || [[ "$action" == "help" ]]; then
|
||
echo -e "$helpmessage"
|
||
exit 0
|
||
fi
|
||
|
||
# Visudo message
|
||
if [[ "$action" == "visudo" ]]; then
|
||
|
||
# User to set folder ownership to is $setting if it is defined and $USER otherwise
|
||
if [[ -z "$setting" ]]; then
|
||
setting=$USER
|
||
fi
|
||
|
||
# Set visudo tempfile ownership to current user
|
||
log "Setting visudo file permissions to $setting"
|
||
sudo chown -R $setting $configfolder
|
||
|
||
# Write the visudo file to a tempfile
|
||
visudo_tmpfile="$configfolder/visudo.tmp"
|
||
sudo rm visudo_tmpfile 2>/dev/null
|
||
echo -e "$visudoconfig" >$visudo_tmpfile
|
||
|
||
# If the visudo file is the same (no error, exit code 0), set the permissions just
|
||
if sudo cmp $visudo_file $visudo_tmpfile &>/dev/null; then
|
||
|
||
echo "The existing battery visudo file is what it should be for version $BATTERY_CLI_VERSION"
|
||
|
||
# Check if file permissions are correct, if not, set them
|
||
current_visudo_file_permissions=$(stat -f "%Lp" $visudo_file)
|
||
if [[ "$current_visudo_file_permissions" != "440" ]]; then
|
||
sudo chmod 440 $visudo_file
|
||
fi
|
||
|
||
# exit because no changes are needed
|
||
exit 0
|
||
|
||
fi
|
||
|
||
# Validate that the visudo tempfile is valid
|
||
if sudo visudo -c -f $visudo_tmpfile &>/dev/null; then
|
||
|
||
# If the visudo folder does not exist, make it
|
||
if ! test -d "$visudo_folder"; then
|
||
sudo mkdir -p "$visudo_folder"
|
||
fi
|
||
|
||
# Copy the visudo file from tempfile to live location
|
||
sudo cp $visudo_tmpfile $visudo_file
|
||
|
||
# Delete tempfile
|
||
rm $visudo_tmpfile
|
||
|
||
# Set correct permissions on visudo file
|
||
sudo chmod 440 $visudo_file
|
||
|
||
echo "Visudo file updated successfully"
|
||
|
||
else
|
||
echo "Error validating visudo file, this should never happen:"
|
||
sudo visudo -c -f $visudo_tmpfile
|
||
fi
|
||
|
||
exit 0
|
||
fi
|
||
|
||
# Reinstall helper
|
||
if [[ "$action" == "reinstall" ]]; then
|
||
echo "This will run curl -sS https://raw.githubusercontent.com/actuallymentor/battery/main/setup.sh | bash"
|
||
if [[ ! "$setting" == "silent" ]]; then
|
||
echo "Press any key to continue"
|
||
read
|
||
fi
|
||
curl -sS https://raw.githubusercontent.com/actuallymentor/battery/main/setup.sh | bash
|
||
exit 0
|
||
fi
|
||
|
||
# Update helper
|
||
if [[ "$action" == "update" ]]; then
|
||
|
||
# Check if we have the most recent version
|
||
if curl -sS https://raw.githubusercontent.com/actuallymentor/battery/main/battery.sh | grep -q "$BATTERY_CLI_VERSION"; then
|
||
echo "No need to update, offline version number $BATTERY_CLI_VERSION matches remote version number"
|
||
else
|
||
echo "This will run curl -sS https://raw.githubusercontent.com/actuallymentor/battery/main/update.sh | bash"
|
||
if [[ ! "$setting" == "silent" ]]; then
|
||
echo "Press any key to continue"
|
||
read
|
||
fi
|
||
curl -sS https://raw.githubusercontent.com/actuallymentor/battery/main/update.sh | bash
|
||
fi
|
||
exit 0
|
||
fi
|
||
|
||
# Uninstall helper
|
||
if [[ "$action" == "uninstall" ]]; then
|
||
|
||
if [[ ! "$setting" == "silent" ]]; then
|
||
echo "This will enable charging, and remove the smc tool and battery script"
|
||
echo "Press any key to continue"
|
||
read
|
||
fi
|
||
enable_charging
|
||
disable_discharging
|
||
$battery_binary remove_daemon
|
||
sudo rm -v "$binfolder/smc" "$binfolder/battery" $visudo_file
|
||
sudo rm -v -r "$configfolder"
|
||
pkill -f "/usr/local/bin/battery.*"
|
||
exit 0
|
||
fi
|
||
|
||
# Charging on/off controller
|
||
if [[ "$action" == "charging" ]]; then
|
||
|
||
log "Setting $action to $setting"
|
||
|
||
# Disable running daemon
|
||
$battery_binary maintain stop
|
||
|
||
# Set charging to on and off
|
||
if [[ "$setting" == "on" ]]; then
|
||
enable_charging
|
||
elif [[ "$setting" == "off" ]]; then
|
||
disable_charging
|
||
else
|
||
log "Error: $setting is not \"on\" or \"off\"."
|
||
exit 1
|
||
fi
|
||
|
||
exit 0
|
||
|
||
fi
|
||
|
||
# Discharge on/off controller
|
||
if [[ "$action" == "adapter" ]]; then
|
||
|
||
log "Setting $action to $setting"
|
||
|
||
# Disable running daemon
|
||
$battery_binary maintain stop
|
||
|
||
# Set charging to on and off
|
||
if [[ "$setting" == "on" ]]; then
|
||
enable_discharging
|
||
elif [[ "$setting" == "off" ]]; then
|
||
disable_discharging
|
||
else
|
||
log "Error: $setting is not \"on\" or \"off\"."
|
||
exit 1
|
||
fi
|
||
|
||
exit 0
|
||
|
||
fi
|
||
|
||
# Charging on/off controller
|
||
if [[ "$action" == "charge" ]]; then
|
||
|
||
if ! valid_percentage "$setting"; then
|
||
log "Error: $setting is not a valid setting for battery charge. Please use a number between 0 and 100"
|
||
exit 1
|
||
fi
|
||
|
||
# Disable running daemon
|
||
$battery_binary maintain stop
|
||
|
||
# Disable charge blocker if enabled
|
||
$battery_binary adapter on
|
||
|
||
# Start charging
|
||
battery_percentage=$(get_battery_percentage)
|
||
log "Charging to $setting% from $battery_percentage%"
|
||
enable_charging # also disables discharging
|
||
|
||
# Loop until battery percent is exceeded
|
||
while [[ "$battery_percentage" -lt "$setting" ]]; do
|
||
|
||
if [[ "$battery_percentage" -ge "$((setting - 3))" ]]; then
|
||
sleep 20
|
||
else
|
||
caffeinate -is sleep 60
|
||
fi
|
||
|
||
done
|
||
|
||
disable_charging
|
||
log "Charging completed at $battery_percentage%"
|
||
|
||
exit 0
|
||
|
||
fi
|
||
|
||
# Discharging on/off controller
|
||
if [[ "$action" == "discharge" ]]; then
|
||
|
||
if ! valid_percentage "$setting"; then
|
||
log "Error: $setting is not a valid setting for battery discharge. Please use a number between 0 and 100"
|
||
exit 1
|
||
fi
|
||
|
||
# Start charging
|
||
battery_percentage=$(get_battery_percentage)
|
||
log "Discharging to $setting% from $battery_percentage%"
|
||
enable_discharging
|
||
|
||
# Loop until battery percent is exceeded
|
||
while [[ "$battery_percentage" -gt "$setting" ]]; do
|
||
|
||
log "Battery at $battery_percentage% (target $setting%)"
|
||
caffeinate -is sleep 60
|
||
battery_percentage=$(get_battery_percentage)
|
||
|
||
done
|
||
|
||
disable_discharging
|
||
log "Discharging completed at $battery_percentage%"
|
||
|
||
fi
|
||
|
||
# Maintain at level
|
||
if [[ "$action" == "maintain_synchronous" ]]; then
|
||
|
||
# Checking if the calibration process is running
|
||
if test -f "$calibrate_pidfile"; then
|
||
pid=$(cat "$calibrate_pidfile" 2>/dev/null)
|
||
kill $calibrate_pidfile &>/dev/null
|
||
log "🚨 Calibration process have been stopped"
|
||
fi
|
||
|
||
# Recover old maintain status if old setting is found
|
||
if [[ "$setting" == "recover" ]]; then
|
||
|
||
# Before doing anything, log out environment details as a debugging trail
|
||
log "Debug trail. User: $USER, config folder: $configfolder, logfile: $logfile, file called with 1: $1, 2: $2"
|
||
|
||
maintain_percentage=$(cat $maintain_percentage_tracker_file 2>/dev/null)
|
||
if [[ $maintain_percentage ]]; then
|
||
log "Recovering maintenance percentage $maintain_percentage"
|
||
setting=$(echo $maintain_percentage)
|
||
else
|
||
log "No setting to recover, exiting"
|
||
exit 0
|
||
fi
|
||
fi
|
||
|
||
if ! valid_percentage "$setting"; then
|
||
log "Error: $setting is not a valid setting for battery maintain. Please use a number between 0 and 100"
|
||
exit 1
|
||
fi
|
||
|
||
# Check if the user requested that the battery maintenance first discharge to the desired level
|
||
if [[ "$subsetting" == "--force-discharge" ]]; then
|
||
# Before we start maintaining the battery level, first discharge to the target level
|
||
log "Triggering discharge to $setting before enabling charging limiter"
|
||
$battery_binary discharge "$setting"
|
||
log "Discharge pre battery-maintenance complete, continuing to battery maintenance loop"
|
||
else
|
||
log "Not triggering discharge as it is not requested"
|
||
fi
|
||
|
||
# Start charging
|
||
battery_percentage=$(get_battery_percentage)
|
||
|
||
log "Charging to and maintaining at $setting% from $battery_percentage%"
|
||
|
||
# Loop until battery percent is exceeded
|
||
while true; do
|
||
|
||
# Keep track of status
|
||
is_charging=$(get_smc_charging_status)
|
||
ac_attached=$(get_charger_state)
|
||
|
||
if [[ "$battery_percentage" -ge "$setting" && ("$is_charging" == "enabled" || "$ac_attached" == "1") ]]; then
|
||
|
||
log "Charge above $setting"
|
||
if [[ "$is_charging" == "enabled" ]]; then
|
||
disable_charging
|
||
fi
|
||
change_magsafe_led_color "green"
|
||
|
||
elif [[ "$battery_percentage" -lt "$setting" && "$is_charging" == "disabled" ]]; then
|
||
|
||
log "Charge below $setting"
|
||
enable_charging
|
||
change_magsafe_led_color "orange"
|
||
|
||
fi
|
||
|
||
sleep 60
|
||
|
||
battery_percentage=$(get_battery_percentage)
|
||
|
||
done
|
||
|
||
exit 0
|
||
|
||
fi
|
||
|
||
# Maintain at voltage
|
||
if [[ "$action" == "maintain_voltage_synchronous" ]]; then
|
||
|
||
# Recover old maintain status if old setting is found
|
||
if [[ "$setting" == "recover" ]]; then
|
||
|
||
# Before doing anything, log out environment details as a debugging trail
|
||
log "Debug trail. User: $USER, config folder: $configfolder, logfile: $logfile, file called with 1: $1, 2: $2"
|
||
|
||
maintain_voltage=$(cat $maintain_voltage_tracker_file 2>/dev/null)
|
||
if [[ $maintain_voltage ]]; then
|
||
log "Recovering maintenance voltage $maintain_voltage"
|
||
setting=$(echo $maintain_voltage | awk '{print $1}')
|
||
subsetting=$(echo $maintain_voltage | awk '{print $2}')
|
||
else
|
||
log "No setting to recover, exiting"
|
||
exit 0
|
||
fi
|
||
fi
|
||
|
||
voltage=$(get_voltage)
|
||
lower_voltage=$(echo "$setting - $subsetting" | bc -l)
|
||
upper_voltage=$(echo "$setting + $subsetting" | bc -l)
|
||
log "Keeping voltage between ${lower_voltage}V and ${upper_voltage}V"
|
||
|
||
# Loop
|
||
while true; do
|
||
is_charging=$(get_smc_charging_status)
|
||
|
||
if (($(echo "$voltage < $lower_voltage" | bc -l))) && [[ "$is_charging" == "disabled" ]]; then
|
||
log "Battery at ${voltage}V"
|
||
enable_charging
|
||
fi
|
||
if (($(echo "$voltage >= $upper_voltage" | bc -l))) && [[ "$is_charging" == "enabled" ]]; then
|
||
log "Battery at ${voltage}V"
|
||
disable_charging
|
||
fi
|
||
|
||
sleep 60
|
||
|
||
voltage=$(get_voltage)
|
||
|
||
done
|
||
|
||
exit 0
|
||
|
||
fi
|
||
|
||
# Asynchronous battery level maintenance
|
||
if [[ "$action" == "maintain" ]]; then
|
||
|
||
# Kill old process silently
|
||
if test -f "$pidfile"; then
|
||
log "Killing old maintain process at $(cat $pidfile)"
|
||
pid=$(cat "$pidfile" 2>/dev/null)
|
||
kill $pid &>/dev/null
|
||
fi
|
||
|
||
if test -f "$calibrate_pidfile"; then
|
||
pid=$(cat "$calibrate_pidfile" 2>/dev/null)
|
||
kill $calibrate_pidfile &>/dev/null
|
||
log "🚨 Calibration process have been stopped"
|
||
fi
|
||
|
||
if [[ "$setting" == "stop" ]]; then
|
||
log "Killing running maintain daemons & enabling charging as default state"
|
||
rm $pidfile 2>/dev/null
|
||
$battery_binary disable_daemon
|
||
enable_charging
|
||
$battery_binary status
|
||
exit 0
|
||
fi
|
||
|
||
# Check if setting is a voltage
|
||
is_voltage=false
|
||
if valid_voltage "$setting"; then
|
||
setting="${setting//V/}"
|
||
|
||
if valid_voltage "$subsetting"; then
|
||
subsetting="${subsetting//V/}"
|
||
else
|
||
subsetting="0.1"
|
||
fi
|
||
|
||
if (($(echo "$setting < $voltage_min" | bc -l) || $(echo "$setting > $voltage_max" | bc -l))); then
|
||
log "Error: ${setting}V is not a valid setting. Please use a value between ${voltage_min}V and ${voltage_max}V"
|
||
exit 1
|
||
fi
|
||
if (($(echo "$subsetting < $voltage_hyst_min" | bc -l) || $(echo "$subsetting > $voltage_max" | bc -l))); then
|
||
log "Error: ${subsetting}V is not a valid setting. Please use a value between ${voltage_hyst_min}V and ${voltage_hyst_max}V"
|
||
exit 1
|
||
fi
|
||
|
||
is_voltage=true
|
||
|
||
# Check if setting is value between 0 and 100
|
||
elif ! valid_percentage "$setting"; then
|
||
log "Called with $setting $action"
|
||
# If non 0-100 setting is not a special keyword, exit with an error.
|
||
if ! { [[ "$setting" == "stop" ]] || [[ "$setting" == "recover" ]]; }; then
|
||
log "Error: $setting is not a valid setting for battery maintain. Please use a number between 0 and 100, or an action keyword like 'stop' or 'recover'."
|
||
exit 1
|
||
fi
|
||
|
||
fi
|
||
|
||
# Start maintenance script
|
||
if [ "$is_voltage" = true ]; then
|
||
log "Starting battery maintenance at ${setting}V ±${subsetting}V"
|
||
nohup $battery_binary maintain_voltage_synchronous $setting $subsetting >>$logfile &
|
||
else
|
||
log "Starting battery maintenance at $setting% $subsetting"
|
||
nohup $battery_binary maintain_synchronous $setting $subsetting >>$logfile &
|
||
fi
|
||
|
||
# Store pid of maintenance process and setting
|
||
echo $! >$pidfile
|
||
pid=$(cat "$pidfile" 2>/dev/null)
|
||
|
||
if ! [[ "$setting" == "recover" ]]; then
|
||
|
||
rm "$maintain_percentage_tracker_file" "$maintain_voltage_tracker_file" 2>/dev/null
|
||
|
||
if [[ "$is_voltage" = true ]]; then
|
||
log "Writing new setting $setting $subsetting to $maintain_voltage_tracker_file"
|
||
echo "$setting $subsetting" >$maintain_voltage_tracker_file
|
||
log "Maintaining battery at ${setting}V ±${subsetting}V"
|
||
|
||
else
|
||
log "Writing new setting $setting to $maintain_percentage_tracker_file"
|
||
echo $setting >$maintain_percentage_tracker_file
|
||
log "Maintaining battery at $setting%"
|
||
fi
|
||
|
||
fi
|
||
|
||
# Enable the daemon that continues maintaining after reboot
|
||
$battery_binary create_daemon
|
||
|
||
exit 0
|
||
|
||
fi
|
||
|
||
# Battery calibration
|
||
if [[ "$action" == "calibrate_synchronous" ]]; then
|
||
log "Starting calibration"
|
||
|
||
# Stop the maintaining
|
||
battery maintain stop
|
||
|
||
# Discharge battery to 15%
|
||
battery discharge 15
|
||
|
||
while true; do
|
||
log "checking if at 100%"
|
||
# Check if battery level has reached 100%
|
||
if battery status | head -n 1 | grep -q "Battery at 100%"; then
|
||
break
|
||
else
|
||
sleep 300
|
||
continue
|
||
fi
|
||
done
|
||
|
||
# Wait before discharging to target level
|
||
log "reached 100%, maintaining for 1 hour"
|
||
sleep 3600
|
||
|
||
# Discharge battery to 80%
|
||
battery discharge 80
|
||
|
||
# Recover old maintain status
|
||
battery maintain recover
|
||
exit 0
|
||
fi
|
||
|
||
# Asynchronous battery level maintenance
|
||
if [[ "$action" == "calibrate" ]]; then
|
||
# Kill old process silently
|
||
if test -f "$calibrate_pidfile"; then
|
||
pid=$(cat "$calibrate_pidfile" 2>/dev/null)
|
||
kill $pid &>/dev/null
|
||
fi
|
||
|
||
if [[ "$setting" == "stop" ]]; then
|
||
log "Killing running calibration daemon"
|
||
kill $calibrate_pidfile &>/dev/null
|
||
rm $calibrate_pidfile 2>/dev/null
|
||
|
||
exit 0
|
||
fi
|
||
|
||
# Start calibration script
|
||
log "Starting calibration script"
|
||
nohup battery calibrate_synchronous >>$logfile &
|
||
|
||
# Store pid of calibration process and setting
|
||
echo $! >$calibrate_pidfile
|
||
pid=$(cat "$calibrate_pidfile" 2>/dev/null)
|
||
fi
|
||
|
||
# Status logger
|
||
if [[ "$action" == "status" ]]; then
|
||
|
||
log "Battery at $(get_battery_percentage)% ($(get_remaining_time) remaining), $(get_voltage)V, smc charging $(get_smc_charging_status)"
|
||
if test -f $pidfile; then
|
||
maintain_percentage=$(cat $maintain_percentage_tracker_file 2>/dev/null)
|
||
if [[ $maintain_percentage ]]; then
|
||
maintain_level="$maintain_percentage%"
|
||
else
|
||
maintain_level=$(cat $maintain_voltage_tracker_file 2>/dev/null)
|
||
maintain_level=$(echo "$maintain_level" | awk '{print $1 "V ±" $2 "V"}')
|
||
fi
|
||
log "Your battery is currently being maintained at $maintain_level"
|
||
fi
|
||
exit 0
|
||
|
||
fi
|
||
|
||
# Status logger in csv format
|
||
if [[ "$action" == "status_csv" ]]; then
|
||
|
||
echo "$(get_battery_percentage),$(get_remaining_time),$(get_smc_charging_status),$(get_smc_discharging_status),$(get_maintain_percentage)"
|
||
|
||
fi
|
||
|
||
# launchd daemon creator, inspiration: https://www.launchd.info/
|
||
if [[ "$action" == "create_daemon" ]]; then
|
||
|
||
call_action="maintain_synchronous"
|
||
if test -f "$maintain_voltage_tracker_file"; then
|
||
call_action="maintain_voltage_synchronous"
|
||
fi
|
||
|
||
daemon_definition="
|
||
<?xml version=\"1.0\" encoding=\"UTF-8\"?>
|
||
<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">
|
||
<plist version=\"1.0\">
|
||
<dict>
|
||
<key>Label</key>
|
||
<string>com.battery.app</string>
|
||
<key>ProgramArguments</key>
|
||
<array>
|
||
<string>$binfolder/battery</string>
|
||
<string>$call_action</string>
|
||
<string>recover</string>
|
||
</array>
|
||
<key>StandardOutPath</key>
|
||
<string>$logfile</string>
|
||
<key>StandardErrorPath</key>
|
||
<string>$logfile</string>
|
||
<key>RunAtLoad</key>
|
||
<true/>
|
||
</dict>
|
||
</plist>
|
||
"
|
||
|
||
mkdir -p "${daemon_path%/*}"
|
||
|
||
# check if daemon already exists
|
||
if test -f "$daemon_path"; then
|
||
|
||
log "Daemon already exists, checking for differences"
|
||
daemon_definition_difference=$(diff --brief --ignore-space-change --strip-trailing-cr --ignore-blank-lines <(cat "$daemon_path" 2>/dev/null) <(echo "$daemon_definition"))
|
||
|
||
# remove leading and trailing whitespaces
|
||
daemon_definition_difference=$(echo "$daemon_definition_difference" | xargs)
|
||
if [[ "$daemon_definition_difference" != "" ]]; then
|
||
|
||
log "daemon_definition changed: replace with new definitions"
|
||
echo "$daemon_definition" >"$daemon_path"
|
||
|
||
fi
|
||
else
|
||
|
||
# daemon not available, create new launch deamon
|
||
log "Daemon does not yet exist, creating daemon file at $daemon_path"
|
||
echo "$daemon_definition" >"$daemon_path"
|
||
|
||
fi
|
||
|
||
# enable daemon
|
||
launchctl enable "gui/$(id -u $USER)/com.battery.app"
|
||
exit 0
|
||
|
||
fi
|
||
|
||
# Disable daemon
|
||
if [[ "$action" == "disable_daemon" ]]; then
|
||
|
||
log "Disabling daemon at gui/$(id -u $USER)/com.battery.app"
|
||
launchctl disable "gui/$(id -u $USER)/com.battery.app"
|
||
exit 0
|
||
|
||
fi
|
||
|
||
# Remove daemon
|
||
if [[ "$action" == "remove_daemon" ]]; then
|
||
|
||
rm $daemon_path 2>/dev/null
|
||
exit 0
|
||
|
||
fi
|
||
|
||
# Display logs
|
||
if [[ "$action" == "logs" ]]; then
|
||
|
||
amount="${2:-100}"
|
||
|
||
echo -e "👾 Battery CLI logs:\n"
|
||
tail -n $amount $logfile
|
||
|
||
echo -e "\n🖥️ Battery GUI logs:\n"
|
||
tail -n $amount "$configfolder/gui.log"
|
||
|
||
echo -e "\n📁 Config folder details:\n"
|
||
ls -lah $configfolder
|
||
|
||
echo -e "\n⚙️ Battery data:\n"
|
||
$battery_binary status
|
||
$battery_binary | grep -E "v\d.*"
|
||
|
||
exit 0
|
||
|
||
fi
|