diff --git a/.DS_Store b/.DS_Store
new file mode 100644
index 0000000..74a78db
Binary files /dev/null and b/.DS_Store differ
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 2296389..febebe8 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -10,7 +10,7 @@
"typescriptreact"
],
"editor.codeActionsOnSave": {
- "source.fixAll.eslint": true
+ "source.fixAll.eslint": "explicit"
},
"editor.renderWhitespace": "all",
"editor.formatOnSave": true,
diff --git a/README.md b/README.md
index 8496fa1..b32cc2d 100644
--- a/README.md
+++ b/README.md
@@ -10,15 +10,17 @@ This is an app for Apple Silicon Macs. It will not work on Intel macs. Do you ha
### Installation
-- Option 1: install through brew with `brew install battery`
-- Option 2: [You can download the latest app dmg version here](https://github.com/actuallymentor/battery/releases/).
-- Option 3: command-line only installation (see section below)
+- Option 1: install the app through brew with `brew install battery`
+- Option 2: [download the app dmg version here](https://github.com/actuallymentor/battery/releases/)
+- Option 3: install ONLY the command line interface (see section below)
+
+When installing via brew or dmg, opening the macOS app is required to complete the installation.
The first time you open the app, it will ask for your administator password so it can install the needed components. Please note that the app:
- Discharges your battery until it reaches 80%, **even when plugged in**
- Disables charging when your battery is above 80% charged
-- Enabled charging when your battery is under 80% charged
+- Enables charging when your battery is under 80% charged
- Keeps the limit engaged even after rebooting
- Keeps the limit engaged even after closing the tray app
- Also automatically installs the `battery` command line tool. If you want a custom charging percentage, the CLI is the only way to do that.
diff --git a/app/.DS_Store b/app/.DS_Store
new file mode 100644
index 0000000..563238f
Binary files /dev/null and b/app/.DS_Store differ
diff --git a/app/modules/battery.js b/app/modules/battery.js
index a03dcf8..818a772 100644
--- a/app/modules/battery.js
+++ b/app/modules/battery.js
@@ -4,7 +4,7 @@ const { exec } = require( 'node:child_process' )
const { log, alert, wait, confirm } = require( './helpers' )
const { get_force_discharge_setting } = require( './settings' )
const { USER } = process.env
-const path_fix = 'PATH=/bin:/usr/bin:/usr/local/bin:/usr/sbin:/opt/homebrew/bin:/opt/homebrew/sbin:/opt/homebrew'
+const path_fix = 'PATH=/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin'
const battery = `${ path_fix } battery`
const shell_options = {
shell: '/bin/bash',
diff --git a/battery.sh b/battery.sh
index 3a17f7d..be2c3a3 100755
--- a/battery.sh
+++ b/battery.sh
@@ -4,10 +4,10 @@
## Update management
## variables are used by this binary as well at the update script
## ###############
-BATTERY_CLI_VERSION="v1.1.7"
+BATTERY_CLI_VERSION="v1.2.2"
# Path fixes for unexpected environments
-PATH=/bin:/usr/bin:/usr/local/bin:/usr/sbin:/opt/homebrew/bin:/opt/homebrew/sbin:/opt/homebrew
+PATH=/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
## ###############
## Variables
@@ -19,7 +19,15 @@ 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
+
+# Voltage limits
+voltage_min="10.5"
+voltage_max="12.6"
+voltage_hyst_min="0.1"
+voltage_hyst_max="2"
## ###############
## Housekeeping
@@ -51,12 +59,18 @@ Usage:
output logs of the battery CLI and GUI
eg: battery logs 100
- battery maintain LEVEL[1-100,stop]
+ 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
@@ -65,6 +79,9 @@ Usage:
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
@@ -105,6 +122,7 @@ ALL ALL = NOPASSWD: LEDCONTROL
"
# Get parameters
+battery_binary=$0
action=$1
setting=$2
subsetting=$3
@@ -117,6 +135,14 @@ 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
+}
+
## #################
## SMC Manipulation
## #################
@@ -124,6 +150,7 @@ function log() {
# 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)
@@ -135,11 +162,14 @@ function change_magsafe_led_color() {
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
}
@@ -158,16 +188,26 @@ function disable_discharging() {
# Keep track of status
is_charging=$(get_smc_charging_status)
- if [[ "$battery_percentage" -ge "$setting" && "$is_charging" == "enabled" ]]; then
+ if ! valid_percentage "$setting"; then
- log "Charge above $setting"
+ 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
+ sudo smc -k CH0B -w 00
+ sudo smc -k CH0C -w 00
+ 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 "Charge below $setting"
- enable_charging
+ log "Disabling discharging: Charge below $setting, enabling charging"
+ # use direct commands since enable_charging also calls disable_discharging, and causes an eternal loop
+ sudo smc -k CH0B -w 00
+ sudo smc -k CH0C -w 00
change_magsafe_led_color "orange"
fi
@@ -223,11 +263,21 @@ function get_remaining_time() {
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
## ###############
@@ -326,7 +376,7 @@ if [[ "$action" == "uninstall" ]]; then
fi
enable_charging
disable_discharging
- battery remove_daemon
+ $battery_binary remove_daemon
sudo rm -v "$binfolder/smc" "$binfolder/battery" $visudo_file
sudo rm -v -r "$configfolder"
pkill -f "/usr/local/bin/battery.*"
@@ -339,13 +389,16 @@ if [[ "$action" == "charging" ]]; then
log "Setting $action to $setting"
# Disable running daemon
- battery maintain stop
+ $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
@@ -358,13 +411,16 @@ if [[ "$action" == "adapter" ]]; then
log "Setting $action to $setting"
# Disable running daemon
- battery maintain stop
+ $battery_binary maintain stop
# Set charging to on and off
if [[ "$setting" == "on" ]]; then
- disable_discharging
- elif [[ "$setting" == "off" ]]; then
enable_discharging
+ elif [[ "$setting" == "off" ]]; then
+ disable_discharging
+ else
+ log "Error: $setting is not \"on\" or \"off\"."
+ exit 1
fi
exit 0
@@ -374,29 +430,30 @@ fi
# Charging on/off controller
if [[ "$action" == "charge" ]]; then
- # Check if percentage is an integer [1-100]
- if ! [[ $setting =~ ^[1-9][0-9]?$|^100$ ]]; then
- log "Specified percentage ($setting) is not valid. Please specify an integer [1-100]."
+ 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 maintain stop
+ $battery_binary maintain stop
# Disable charge blocker if enabled
- battery adapter on
+ $battery_binary adapter on
# Start charging
battery_percentage=$(get_battery_percentage)
log "Charging to $setting% from $battery_percentage%"
- enable_charging
+ enable_charging # also disables discharging
# Loop until battery percent is exceeded
while [[ "$battery_percentage" -lt "$setting" ]]; do
- log "Battery at $battery_percentage%"
- caffeinate -is sleep 60
- battery_percentage=$(get_battery_percentage)
+ if [[ "$battery_percentage" -ge "$((setting - 3))" ]]; then
+ sleep 20
+ else
+ caffeinate -is sleep 60
+ fi
done
@@ -410,6 +467,11 @@ 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%"
@@ -432,6 +494,18 @@ 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
+
+ if ! validate_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
+
# Recover old maintain status if old setting is found
if [[ "$setting" == "recover" ]]; then
@@ -452,7 +526,7 @@ if [[ "$action" == "maintain_synchronous" ]]; then
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 discharge "$setting"
+ $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"
@@ -468,11 +542,14 @@ if [[ "$action" == "maintain_synchronous" ]]; then
# Keep track of status
is_charging=$(get_smc_charging_status)
+ ac_attached=$(get_charger_state)
- if [[ "$battery_percentage" -ge "$setting" && "$is_charging" == "enabled" ]]; then
+ if [[ "$battery_percentage" -ge "$setting" && ("$is_charging" == "enabled" || "$ac_attached" == "1") ]]; then
log "Charge above $setting"
- disable_charging
+ if [[ "$is_charging" == "enabled" ]]; then
+ disable_charging
+ fi
change_magsafe_led_color "green"
elif [[ "$battery_percentage" -lt "$setting" && "$is_charging" == "disabled" ]]; then
@@ -493,6 +570,54 @@ if [[ "$action" == "maintain_synchronous" ]]; then
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
@@ -502,19 +627,45 @@ if [[ "$action" == "maintain" ]]; then
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 disable_daemon
- enable_charging
- change_magsafe_led_color
- battery status
+ $battery_binary disable_daemon
+ $battery_binary status
exit 0
fi
- # Check if setting is value between 0 and 100
- if ! [[ "$setting" =~ ^[0-9]+$ ]] || [[ "$setting" -lt 0 ]] || [[ "$setting" -gt 100 ]]; then
+ # Check if setting is a voltage
+ is_voltage=false
+ if [[ "$setting" =~ ^[0-9]+(\.[0-9]+)?V$ ]]; then
+ setting="${setting//V/}"
+ if [[ "$subsetting" =~ ^[0-9]+(\.[0-9]+)?V$ ]]; 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
+ fi
+
+ # Check if setting is value between 0 and 100
+ if ! 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
@@ -526,32 +677,114 @@ if [[ "$action" == "maintain" ]]; then
# Start maintenance script
log "Starting battery maintenance at $setting% $subsetting"
- nohup battery maintain_synchronous $setting $subsetting >>$logfile &
+ nohup $battery_binary maintain_synchronous $setting $subsetting >>$logfile &
+ if [ "$is_voltage" = true ]; then
+ log "Starting battery maintenance at ${setting}V ±${subsetting}V"
+ nohup battery maintain_voltage_synchronous $setting $subsetting >>$logfile &
+ else
+ log "Starting battery maintenance at $setting% $subsetting"
+ nohup battery 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
- log "Writing new setting $setting to $maintain_percentage_tracker_file"
- echo $setting >$maintain_percentage_tracker_file
- log "Maintaining battery at $setting%"
+
+ 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 create_daemon
+ $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), smc charging $(get_smc_charging_status)"
+ 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)
- log "Your battery is currently being maintained at $maintain_percentage%"
+ 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
@@ -567,6 +800,11 @@ 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="
@@ -577,7 +815,7 @@ if [[ "$action" == "create_daemon" ]]; then
ProgramArguments
$binfolder/battery
- maintain_synchronous
+ $call_action
recover
StandardOutPath
@@ -645,15 +883,15 @@ if [[ "$action" == "logs" ]]; then
echo -e "👾 Battery CLI logs:\n"
tail -n $amount $logfile
- echo -e "\n🖥️ Battery GUI logs:\n"
+ 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 status
- battery | grep -E "v\d.*"
+ echo -e "\n⚙️ Battery data:\n"
+ $battery_binary status
+ $battery_binary | grep -E "v\d.*"
exit 0