Suspend a program when on battery power [duplicate]

I haven't found anything yet so I was hoping maybe someone here had an idea.

I want to kick off an even based on battery on an Apple laptop reaching a certain charge level, or when the battery is fully charged.

I know I can have something run periodically, check the battery level, and decide what to do, but I prefer event driven if possible.


Well, this question is old but I ran into a similar situation and wanted to run a shortcut (with "Siri Shortcuts") on macOS 12 on certain battery levels.

Instead of a background service I decided a script would work better for me since it should turn on and off my HomeKit outlet that the charger is connected to. (But only while the MacBook is transcoding a movie)

From Joseph's answer I took the bits that still work and created a Tcl script that checks the battery level every minute.

I have created a file named batt.tcl with the following content:

#!/usr/bin/env tclsh

set run 1
set last 0
set logs 1
set logfile "batt.log.txt"
set settingsfile batt.cfg
set min 15
set max 80
set delay 10000

# leave "nil"; we need a wake lock when we're charging. This is done using a call "caffeinate",     waiting for another PID to be removed.
set wakeLock "nil"

proc log {message} {
  if {$::logs != 1} {
    return
  }
  set msg ""
  append msg [clock format [clock seconds] -format "%d.%m.%Y %H:%M:%S"]
  append msg ": "
  append msg $message
  set of1 [open $::logfile a]
  puts $of1 $msg
  close $of1
}

proc enableWakeLock {} {
  if {$::wakeLock == "nil"} {
    log "Enabling wake lock"
        set ::wakeLock [open "|tclsh" r+]
    set myProcess [pid $::wakeLock]
    exec caffeinate -i -w $myProcess &
  }
}

proc disableWakeLock {} {
  if {$::wakeLock != "nil"} {
    log "Disabling wake lock"
    puts $::wakeLock "exit\n"
    flush $::wakeLock
    close $::wakeLock
    set ::wakeLock "nil"
  }
}

proc replaceAll {input lookup replacement} {
  set offs 0
  set mapping [list $lookup $replacement]
  while {$offs != -1} {
    set input [string map $mapping $input]
    set offs [string first $lookup $input]
  }
  return $input
}

proc loadSettings {} {
  if {[file exists $::settingsfile]} {
    set if1 [open $::settingsfile]
    set data [read $if1]
    close $if1

    set data [string map [list \t " " \n " " \r " "] $data]
    set data [replaceAll $data "  " " "]
    set data [string trim $data " "]

    set lines [split $data " "]
    set id ""
    foreach line $lines {
      if {[string index $line 0] != "#"} {
        if {[string length $id] == 0} {
          set id $line
        } else {
          switch -exact -- $id {
            "low:" {
              set ::min $line
              set id ""
            }
            "high:" {
              set ::max $line
              set id ""
            }
            "logs:" {
              set ::logs $line
              set id ""
            }
            "logfile:" {
              set ::logfile $line
              set id ""
            }
            default {
              puts "Unknown config option <$id>. Aborting."
              exit
            }
          }
        }
      }
    }
  } else {
    puts -nonewline "Generating settings file"
    flush stdout
    set of1 [open $::settingsfile w]
    puts $of1 [list "low:" $::min]
    puts $of1 [list "high:" $::max]
    puts $of1 [list "logfile:" $::logfile]
    puts -nonewline $of1 [list "logs:" $::logs]
    flush $of1
    close $of1
    puts ", done."
  }

  if {$::logs != 1 && $::logs != 0} {
    set msg "Weird \"logs:\" setting: must be 1 or 0. Aborting."
    puts $msg
    exit
  }

  if {$::min >= $::max} {
    set msg "Weird configuration: low charge ($::min) is equal or higher that high charge ($::max). Aborting."
    puts $msg
    exit
  }

}

proc waypointSearch {input waypoints endwaypoint} {
  set offs 0
  set len 0
  foreach wp $waypoints {
    set len [string length $wp]
    set offs [string first $wp $input $offs]
    if {$offs < 0} {
      puts "Waypoint \"$wp\" not found."
    }
#    puts "WP: <$wp> $offs $len"
  }
  set eoffs [string first $endwaypoint $input $offs]
  if {$eoffs < 0} {
    puts "Waypoint \"$endwaypoint\" not found."
  }
  return [string range $input [expr $offs + $len] [expr $eoffs - 1]]
}


loadSettings

set msg "Starting with settngs for low: $min and high: $max."
puts -nonewline "\n\nlow: $min, high: $max"
log $msg

while {$run == 1} {

  set input [exec ioreg -l]
  set lvl [waypointSearch $input [list "AppleSmartBattery " "\"CurrentCapacity\"" "="] "\n"]
  set level [string trim $lvl " \t\n"]

  set changes 0

  if {$last != $level} {
    set v [expr abs(abs($last - $level)-1) * -10000]
    set wouldBe [expr $delay + $v]
    if {$v < 0 && $wouldBe > 0} {
      set delay $wouldBe
      set changes 1
    }

    if {$level <= $::min} {
      set last $level
      set msg "Batt low: $level"
      puts -nonewline "\n" 
      puts -nonewline $msg
      log $msg
      enableWakeLock
      exec shortcuts run mac-battery-low
      continue
    } elseif {$level >= $::max} {
      set last $level
      set msg "Batt high: $level"
      puts -nonewline "\n" 
      puts -nonewline $msg
      log $msg
      exec shortcuts run mac-battery-high
      disableWakeLock
      continue
    }
  } else {
    set canMaxBe [expr ($level*3000)]
    if {$delay + 10000 <= $canMaxBe} {
      incr delay 10000
      set changes 1
    }
  }

  if {$last != $level} {
    set changes 1
  }

  set last $level
  if {$changes == 1} {
    puts -nonewline "\n" 
    puts -nonewline "Batt: $level"
    puts -nonewline " ([expr $delay/1000] s)"
    flush stdout
  }
  after $delay
  
}

Then I have created 2 shortcuts for "Siri Shortcuts" and named them mac-battery-low and mac-battery-high. The first one will turn on the outlet, the 2nd one will turn it off.

On Terminal I simply run tclsh batt.tcl (or chmod 755 batt.tcl and double-click the file) to start monitoring the battery. Ctrl + C will end monitoring.

Edit

Removed.

(This was confusing since the script was updated and has a better solution.)

Edit2

What I have found so far:

  1. The 'caffeinating' process keeps the system up and running.
  2. We should only caffeinate the system while we're charging since there are also other processes that caffeinate while the user is active.

-> So there is no need to check pmset -g assertions for existing assertions.

  1. We simply need to create an own assertion that keeps the Mac on while we're charging. When we reached the desired battery level we fire our event to remove the assertion. Any caffeinating process may still be running and keeps the system on. Or it already finished - which allows the Mac to enter sleep right away.

So we allow the system to begin deep sleep anytime if the charger is turned off. However, others might prevent deep sleep and thus keep us up and running, as well. (periodically checking)

I have updated the above script. On first run it creates a settings file that allows you to change the values for later sessions without defining parameters.


I want this to be OS based, so I don't need an internet connection or a browser open etc. Just something that can happen in the background

The easiest way to do it would be with an application, but here's how to do it with built-in OS commands only. I know you want it to be event driven, but I'm not sure how to accomplish that, so here's one way of checking the battery level, then doing something else if it's above/below a threshold.

You may consider a launchd process, this is basically a scheduled task which runs every x minutes. They are often used by advanced users, but aren't too difficult to set up. You configure a launchd scheduled task with a .plist file which you put into this directory: Macintosh HD\Library\LaunchDaemons and this is how you structure the .plist file;

<?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>Debug</key>
<true/>
<key>ExitTimeOut</key>
<integer>120</integer>
<key>Label</key>
<string>com.me.BatteryInfo</string>
<key>ProgramArguments</key>
<array>
    <string>/Users/yourusername/batt.sh</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>StartInterval</key>
<integer>1800</integer>
</dict>
</plist>

If you were to save that file as com.me.BatteryInfo in the LaunchDaemons folder mentioned above, it would create a scheduled task which runs every 30 minutes. The number, 1800, directly above </dict> is the time in seconds of how often you want the task to run. Where it says <string>/Users/yourusername/batt.sh</string> is where you specify which script runs on schedule. You must leave <string> and </sting> intact.

The line <string>com.me.BatteryInfo</string> is the unique name of the scheduled task. If you are going to make more than one, make sure each .plist has a unique name here.

The next thing you need to do is change the ownership of the .plist to root. This is required as a security feature (to stop software/users creating scheduled malicious tasks, I assume). To change the ownership of the file, do sudo chown root \Library\LaunchDaemons\yourtask.plist (replace yourtask.plist with the actual file name of the .plist you created). This task will prompt you for a password.

Now you need to create the script which will run periodically. You need to make a .sh file (a bash script) to tell the computer what to do. To make a .sh file, open up a programmers text editor, such as Sublime Text or Komodo Edit. DO NOT use Text Edit, as it often adds text to your files which would interfere with your script. Text Edit shouldn't really be used for code.

Make a script (.sh file) with the following code;

#!/bin/sh

percent=$(ioreg -l | awk '$3~/Capacity/{c[$3]=$5}END{OFMT="%.3f";max=c["\"MaxCapacity\""];print(max>0?100*c["\"CurrentCapacity\""]/max:"?")}')

if [ $percent > 95 ]
    then
        echo 'charged.'
fi
exit 0

Replace echo 'charged.' with the terminal command(s) you would like to run when the battery is charged. open /Applications/Notes.app will open the Notes application; you can change the directory to open a different application.

$percent > 95 This tells the next line to only run when the battery is more than 95 charged. You can change this to whatever you want. The battery level here will often be slightly different to what is displayed in the menu bar at the top. If you want to 'run when battery is fully charged' I recommend leaving this as > 95. If you want the task to run when your battery goes below 20% for example, change it to $percent < 20

NOTE: Because this is a scheduled task, your script will run every x number of minutes. This means if you put open \Applications\Notes.app inside your script, the Notes application will start every x minutes (if your battery is charged)

This task will run even if nobody is logged in.

I know you asked this question a while ago, but hopefully this will help someone.