13

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.

0

4 Answers 4

2

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.

2
  • Haha - this was so long ago, my whole idea was to have the TeamFortress2 "FULLY CHARGED!" sound byte play whenever my laptop had completed charging - maybe I can get around to it now Dec 22, 2021 at 2:03
  • I, on the other hand, miss the system's battery warning and forget to connect the charger. That's why my Mac is permanently connected to the charger and HomeKit is used to switch the corresponding outlet on and off. It should turn on at 20% and turn off at 80%. This also saves the battery health.
    – Anticro
    Dec 22, 2021 at 6:56
7

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.

5

Power Manager is not free but supports triggering events based on battery levels (internal or UPS). Events can run scripts, launch applications, or perform tasks such as shutting down.

Power Manager is event driven and does not poll for battery changes.

Instead Power Manager hooks into the IOKit layer of OS X and awaits updates from the hardware. Events can be triggered when no-one is logged in; it does not rely on an active user.

Power Manager - Run a script on battery remaining

The built-in Schedule Assistant tasks focus on performing when the battery drops to a particular level, but they can be tweaked to check for rises in battery percentage or time remaining.

These two posts talk about UPS power sources, but they are easily adapted for your MacBook's internal battery:

Disclosure: I am a software engineer behind Power Manager.

0
2

This is not Mac-specific, but Mozilla Aurora has built-in API that contains a variety of battery-related functions. One can detect the status of the battery (whether it's charging or not), how much more time it will take for the battery to discharge/charge, and its level. There's a simple example of how to add an EventListener to call a function when the battery level is at a certain point.

1
  • 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. Jul 29, 2013 at 19:18

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .