Skip to content

Instantly share code, notes, and snippets.

@brandon1024
Last active April 21, 2024 07:09
Show Gist options
  • Save brandon1024/74b81564aa0b91aa8287faaa175593e6 to your computer and use it in GitHub Desktop.
Save brandon1024/74b81564aa0b91aa8287faaa175593e6 to your computer and use it in GitHub Desktop.
Battery Percentage Boundary Notification Background Script for macOS

Battery Percentage Boundary Notification Background Script for macOS

Preface

I'm weird. We all have our weird habits and quirks. Luckily for me, mine only involves my the battery in my macbook computer.

Are you worried about keeping your devices' batteries healthy and keeping a charge? With every device I have owned, battery health has degraded noticeably over time, likely due to my poor charging habits.

I'll be the first to admit, I am no electrochemist. I might be (and likely am) askew, and what I am about to show you may have absolutely no effect on battery performance. But, I like to believe it does :)

I wanted a way to get a notification when my battery reaches 40% and 80%. That's my target range. So I wrote a script to do it (I have no life). It is written in AppleScript, and uses a fancy-pants notification card to display a message when it's time to charge or unplug the charge cable. The script relies on Apple's task scheduler launchd, and by following these instructions, the script will run automatically after login.

Installation

First, you will need to download the two files below BatteryStatusNotification.scpt and battery.monitor.plist. Make sure you edit the plist file to point to the correct script file.

If you want to run the script once, just run osascript BatteryStatusNotification.scpt. This is fine, but won't automatically run the script on login. To do this, copy the plist file battery.monitor.plist to ~/Library/LaunchAgents/. I recommend placing the script under ~/Applications.

Test this out and let me know what you think! You can also easily adjust the boundaries, or modify the script to suit your needs.

Further Reading

Stack Exchange - How to run custom AppleScript in Background

<?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>battery-status-monitor.job</string>
<key>ProgramArguments</key>
<array>
<string>/usr/bin/osascript</string>
<string>PATH/TO/YOUR/SCRIPT/FILE/BatteryStatusNotification.scpt</string>
</array>
<key>RunAtLoad</key>
<true/>
</dict>
</plist>
repeat
set chargeState to do shell script "pmset -g batt | awk '{printf \"%s %s\\n\", $4,$5;exit}'"
set percentLeft to do shell script "pmset -g batt | egrep -ow '([0-9]{1,3})[%]' | egrep -ow '[0-9]{1,3}'"
considering numeric strings
if chargeState contains "Battery Power" and percentLeft ≤ 40 then
display notification "Time to plug me in :)" with title "Battery Charge Boundary"
else if chargeState contains "AC Power" and percentLeft ≥ 80 then
display notification "Time to unplug me :)" with title "Battery Charge Boundary"
end if
end considering
delay 60
end repeat
@floriangaechter
Copy link

This is exactly what I was looking for! Thanks so much for this ❤️

@blakegearin
Copy link

I've been relying on third-party apps for this functionality and for some reason they're very flaky. This seems to be consistent and I actually like that it nags me every minute. I haven't seen any others that do that, maybe because they're afraid of angering users.

One nice-to-have feature that may or may not be possible to implement in an AppleScript is to automatically dismiss low battery notifications on plug in and vice versa.

@blakegearin
Copy link

Here's some code that seems to accomplish what I described above:

repeat
	set chargeState to do shell script "pmset -g batt | awk '{printf \"%s %s\\n\", $4,$5;exit}'"
	set percentLeft to do shell script "pmset -g batt | egrep -ow '([0-9]{1,3})[%]' | egrep -ow '[0-9]{1,3}'"
	considering numeric strings
		if percentLeft  40 then
			if chargeState contains "AC Power" then
				my clear_notifications()
			else
				display notification "Time to plug me in :)" with title "Battery Charge Boundary"
			end if
		else if percentLeft  80 then
			if chargeState contains "Battery Power" then
				my clear_notifications()
			else
				display notification "Time to unplug me :)" with title "Battery Charge Boundary"
			end if
		else
			my clear_notifications()
		end if
	end considering
	delay 60
end repeat
on clear_notifications()
	tell application "System Events" to tell process "NotificationCenter"
		set numwins to (count windows)
		repeat with i from 1 to count of windows
			set number_of_notifications to count of groups of UI element 1 of scroll area 1 of window i
			set j to 0
			repeat number_of_notifications times
				if value of static text 3 of group j of UI element 1 of scroll area 1 of window i is "Battery Charge Boundary" then
					perform (first action of group j of UI element 1 of scroll area 1 of window i where description is "Close")
					delay 0.5
				else
					set j to (j + 1)
				end if
			end repeat
		end repeat
	end tell
end clear_notifications

Notes:

  • I have only tested this with alerts so it may not work properly with banners (may need a different approach/implementation for that due to differences in UI elements)
  • Without the delay after the perform operation there seems to be a race condition between the last close completing and the next close being triggered, which halts operation. Other related code I've found online uses delay as well (1, 2). Though 0.5 is pretty conservative; you could probably get away with 0.2 on a new or fast machine
  • Probably worth mentioning that this is more useful with a smaller delay in the main repeat block, since having to wait a minute for notification dismissal is pretty long; though the shorter the delay the more this script will impact your machine's battery & performance

@alejo2k
Copy link

alejo2k commented Feb 25, 2023

Hey there. I have been looking for something like this for so long and when I finally find it, I can't make it work... The whole auto process did not work for me so I tried running the script and it doesn't work either. What I might be doing wrong? I have attached the scripts so you can see the path where I add them and the code.

Looking forward to hearing from you.

Thanks

2023-02-25_18-38-46
2023-02-25_18-36-50

@brandon1024
Copy link
Author

@alejo2k it's been so long since I've used this; I'm not sure I can help. I'm mostly on Linux now. A few comments though:

  • The script executes a few shell commands. Try running those commands in the terminal and make sure they succeed.
  • In your screenshots, you have 73% battery life. You should only be notified when the battery falls outside of 20% - 80%, so I wouldn't expect notifications anyway.
  • Try a reboot?

@prishs
Copy link

prishs commented May 4, 2023

@brandon1024 thanks for the script. it is working fine..
its just that notifications keeps getting accumulated.. so as suggested by @blakegearin i tried running his code on monterey..
but its not clearing the notifications containing "battery charge boundary"..
I tried looking for apple script documents but did not get clear document on how to click clear button on notification banners using apple script..
pls guide me or point to useful resources if possible..

@mummifiedclown
Copy link

mummifiedclown commented Jun 30, 2023

@brandon1024 Yes, thanks much! I was about to embark on something similar and you saved me a bunch of work. I was just interested in something to monitor charge/discharge state and alert (scream noise...) when it reached ~80% for devices going into storage for several months. So I cobbled together this as an app:

set screamFile to path to resource "Wilhelm.mp3"
try
	repeat
		set chargeState to do shell script "pmset -g batt"
		set percentLeft to do shell script "pmset -g batt | egrep -ow '([0-9]{1,3})[%]' | egrep -ow '[0-9]{1,3}'"
		do shell script "caffeinate -dit 30 > /dev/null 2>&1 &"
		considering numeric strings
			if chargeState contains "Battery Power" and percentLeft < 80 then
				display alert "Charge State" message "Battery is at " & (percentLeft as string) & "%." & return & "Plug in charger to reach optimal charge level (80%)." buttons {"Quit", "Continue"} default button "Continue" cancel button "Quit" giving up after 30
			else if chargeState contains "AC attached" and chargeState contains "not charging" then
				display alert "Charge State" message "Battery charge is holding at " & (percentLeft as string) & "%." buttons {"Quit", "Continue"} default button "Continue" cancel button "Quit" giving up after 30
			else if chargeState contains "AC Power" and percentLeft > 80 then
				display alert "Charge State" message "Battery is at " & (percentLeft as string) & "%." & return & "Unplug charger to reach optimal charge level (80%)." buttons {"Quit", "Continue"} default button "Continue" cancel button "Quit" giving up after 30
			else if chargeState contains "AC Power" and percentLeft < 80 then
				display alert "Charge State" message "Battery is at " & (percentLeft as string) & "% and currently charging to optimal level (80%)." buttons {"Quit", "Continue"} default button "Continue" cancel button "Quit" giving up after 30
			else if chargeState contains "Battery Power" and percentLeft > 80 then
				display alert "Charge State" message "Battery is at " & (percentLeft as string) & "% and discharging." & return & "Please wait for battery to reach optimal charge level (80%)." buttons {"Quit", "Continue"} default button "Continue" cancel button "Quit" giving up after 30
			else if ((percentLeft as string) = "80") then
				do shell script "afplay " & quoted form of (POSIX path of screamFile) & " > /dev/null 2>&1 &"
				display alert "Charge State" message "Battery is now at an optimal charge of " & (percentLeft as string) & "%!" buttons {"Quit", "Continue"} default button "Continue" cancel button "Quit" giving up after 30
			end if
		end considering
	end repeat
end try

BTW, I found that awking the pmset result wasn't strictly necessary as AS's contains will simply search the contents of the string - probably a toss up as far as code efficiency goes..

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment