Skip to content

Instantly share code, notes, and snippets.

@thisdougb
Last active April 26, 2020 16:51
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save thisdougb/51c61703e285541704562af5e7955752 to your computer and use it in GitHub Desktop.
Save thisdougb/51c61703e285541704562af5e7955752 to your computer and use it in GitHub Desktop.
Simple alert to let you know when the device battery is low.
#!/usr/bin/env bash
#
# Pops up a window notification to let you know when your Magic devices are below THRESHOLD. OSX
# gives you a warning at about 2% battery (mouse and keyboard), which means you have to stop work
# when the battery dies. Threshold at 20% gives you a few days of power to fit charging in.
#
# eg:
# ---------------------------------------
# | |
# | Get a coffee and charge: |
# | |
# | Magic Mouse 2 at 18% |
# | Magic Keyboard at 5%. |
# | |
# | |
# | OK Cancel |
# ---------------------------------------
#
#
#
# 1. Save locally as AppleMagicPower.sh, chmod +x <file>
#
# 2. Add a cron entry to run it, for example:
# 30 9 * * * /Users/dougb/dev/scripts/AppleMagicPower.sh
#
# (OSX asks permission to run this the first time)
#
# @thisdougb, 25/04/2020
# You can change the threshold
THRESHOLD=20
# You can change the message, if coffee is not your thing
MESSAGE="Get a coffee and charge:"
# Probably best leave this as is
DEVICES=("Magic Mouse 2" "Magic Keyboard")
# --------------- Change nothing below here ---------------
messages=()
for index in ${!DEVICES[*]}
do
device=${DEVICES[$index]}
# trying to make it readable
powerValue=$(/usr/sbin/ioreg -r -l -n AppleHSBluetoothDevice -a \
| awk -v dev="$device" \
'BEGIN { battery_value = 0 } \
/\<key\>BatteryPercent\<\/key\>/ \
{ \
getline; \
match($0, "[0-9]{1,3}"); \
battery_value = substr($0, RSTART, RLENGTH) \
} \
/\<key\>Product\<\/key\>/ \
{ \
getline; \
if ($0 ~ dev) { \
print battery_value; \
exit 0; \
} else { \
battery_value = 0 \
} \
} \
')
int_re='^[0-9]+$'
if [[ $powerValue =~ $int_re ]] ; then
if [ $powerValue -le $THRESHOLD ]; then
messages[$index]="$device at $powerValue%"
fi
fi
done
len=${#messages[@]}
if (( "$len" > 0 )); then
if [[ -z $MESSAGE ]]; then
message="Get a coffee and charge:"
else
message=$MESSAGE
fi
for index in ${!messages[*]}
do
message="$message\n\t${messages[$index]}"
done
/usr/bin/osascript <<-EOF
tell application "System Events"
activate
display dialog "$message"
end tell
EOF
fi
@phips
Copy link

phips commented Apr 23, 2020

One pipe? /usr/sbin/ioreg -r -l -n AppleHSBluetoothDevice | awk '/BatteryPercent/ { print $4 }'

@thisdougb
Copy link
Author

One pipe? /usr/sbin/ioreg -r -l -n AppleHSBluetoothDevice | awk '/BatteryPercent/ { print $4 }'

Nice one!

I was thinking the other day that I should add the keyboard as well. But then I realised I don't have a wireless keyboard... Would be pretty simple, just need to get the (-n) name.

@phips
Copy link

phips commented Apr 23, 2020

Hmm, it fits under the same -n by the looks of it:

iMac:~$ /usr/sbin/ioreg -r -l -n AppleHSBluetoothDevice | awk '/BatteryPercent/'
  |           "BatteryPercent" = 60
  |           "BatteryPercent" = 36

Let me try and isolate it…

@phips
Copy link

phips commented Apr 23, 2020

I started to mess with a perlre to snaffle the details, but I'm beyond rusty 😆 So, here's the tree output, which I'm betting you can parse …

iMac:~$ /usr/sbin/ioreg -rln AppleDeviceManagementHIDEventService 
+-o AppleDeviceManagementHIDEventService  <class AppleDeviceManagementHIDEventService, id 0x100000725, registered, mat$
    {
      "LowBatteryNotificationPercentage" = 2
      "PrimaryUsagePage" = 65280
      "BatteryFaultNotificationType" = "MOBatteryFault"
      "VersionNumber" = 0
      "VendorID" = 76
      "LastCriticalError" = 32
      "Built-In" = No
      "DeviceAddress" = "94-b0-1f-03-35-0a"
      "IOUserClientClass" = "IOHIDEventServiceUserClient"
      "WakeReason" = "Button (0x03)"
      "Product" = "Magic Mouse 2"
      "SerialNumber" = "94-b0-1f-03-35-0a"
      "Transport" = "Bluetooth"
      "BatteryLowNotificationType" = "MOLowBattery"
      "ProductID" = 617
      "DeviceUsagePairs" = ({"DeviceUsagePage"=65280,"DeviceUsage"=11},{"DeviceUsagePage"=65280,"DeviceUsage"=20})
      "BatteryPercent" = 60
      "BD_ADDR" = <94b01f03350a>
      "CriticallyLowBatteryNotificationPercentage" = 1
      "BatteryStatusNotificationType" = "BatteryStatusChanged"
      "ReportInterval" = 11250
      "VendorIDSource" = 1
      "STFW Version" = 2133
      "CFBundleIdentifier" = "com.apple.driver.AppleTopCaseHIDEventDriver"
      "IOCFPlugInTypes" = {"7DDEECA8-A7B4-11DA-8A0E-0014519758EF"="IOHIDFamily.kext/Contents/PlugIns/IOHIDLib.plugin",$
      "IOProviderClass" = "IOHIDInterface"
      "LocationID" = 520303882
      "IOClass" = "AppleDeviceManagementHIDEventService"
      "HIDServiceSupport" = No
      "CFBundleIdentifierKernel" = "com.apple.driver.AppleTopCaseHIDEventDriver"
      "ProductIDArray" = (617)
      "BatteryStatusFlags" = 0
      "ColorID" = 0
      "IOMatchCategory" = "IODefaultMatchCategory"
      "CountryCode" = 0
      "IOProbeScore" = 7175
      "PrimaryUsage" = 11
      "IOGeneralInterest" = "IOCommand is not serializable"
      "BTFW Version" = 258
    }
    

+-o AppleDeviceManagementHIDEventService  <class AppleDeviceManagementHIDEventService, id 0x10003a084, registered, mat$
    {
      "LowBatteryNotificationPercentage" = 2
      "PrimaryUsagePage" = 65280
      "BatteryFaultNotificationType" = "KBBatteryFault"
      "VersionNumber" = 0
      "VendorID" = 76
      "IOUserClientClass" = "IOHIDEventServiceUserClient"
      "Built-In" = No
      "DeviceAddress" = "68-fe-f7-73-44-2f"
      "WakeReason" = "Keyboard (0x02)"
      "Product" = "Magic Keyboard"
      "SerialNumber" = "68-fe-f7-73-44-2f"
      "Transport" = "Bluetooth"
      "BatteryLowNotificationType" = "KBLowBattery"
      "ProductID" = 615
      "DeviceUsagePairs" = ({"DeviceUsagePage"=65280,"DeviceUsage"=11},{"DeviceUsagePage"=65280,"DeviceUsage"=20})
      "BatteryPercent" = 36
      "BD_ADDR" = <68fef773442f>
      "CriticallyLowBatteryNotificationPercentage" = 1
      "BatteryStatusNotificationType" = "BatteryStatusChanged"
      "ReportInterval" = 11250
      "VendorIDSource" = 1
      "STFW Version" = 2129
      "CFBundleIdentifier" = "com.apple.driver.AppleTopCaseHIDEventDriver"
      "IOCFPlugInTypes" = {"7DDEECA8-A7B4-11DA-8A0E-0014519758EF"="IOHIDFamily.kext/Contents/PlugIns/IOHIDLib.plugin",$
      "IOProviderClass" = "IOHIDInterface"
      "LocationID" = 2004042799
      "IOClass" = "AppleDeviceManagementHIDEventService"
      "HIDServiceSupport" = No
      "CFBundleIdentifierKernel" = "com.apple.driver.AppleTopCaseHIDEventDriver"
      "ProductIDArray" = (615)
      "BatteryStatusFlags" = 0
      "ColorID" = 0
      "IOMatchCategory" = "IODefaultMatchCategory"
      "CountryCode" = 0
      "IOProbeScore" = 7175
      "PrimaryUsage" = 11
      "IOGeneralInterest" = "IOCommand is not serializable"
      "BTFW Version" = 256
    }

@thisdougb
Copy link
Author

hacktastic... 😬

@phips
Copy link

phips commented Apr 24, 2020

Ha! I've realised it's a whole new blob of Python 😆 I'll test it out shortly …

@thisdougb
Copy link
Author

thisdougb commented Apr 24, 2020

yeah, there's no way to distinguish the two dumps of text you have above. So I had to dump it out as xml, then write a searcher for the xml... Apple don't make it easy.

@phips
Copy link

phips commented Apr 24, 2020

Catalina, sigh … 

iMac:~$ python -V
Python 2.7.16

@thisdougb
Copy link
Author

ahh, yeah, I forgot. It's not like Apple haven't had warning.

The End Of Life date (EOL, sunset date) for Python 2.7 has been moved five years into the future, to 2020.

On the plus side. I've always wanted to do xml parsing in bash...

@phips
Copy link

phips commented Apr 25, 2020

Ha! Quite!

@thisdougb
Copy link
Author

This is working for me. Back to bash...

@phips
Copy link

phips commented Apr 25, 2020

Hmm, not sure it's picked the keyboard battery level up…

iMac:scripts$ sh -x !$
sh -x ./magic
+ THRESHOLD=20
+ MESSAGE='Get a coffee and charge:\n'
+ DEVICES=("Magic Mouse 2" "Magic Keyboard")
+ messages=()
+ for index in '${!DEVICES[*]}'
+ device='Magic Mouse 2'
++ /usr/sbin/ioreg -r -l -n AppleHSBluetoothDevice -a
++ awk -v 'dev=Magic Mouse 2' 'BEGIN { battery_key=0; battery_value=0 } /\<key\>BatteryPercent\<\/key\>/ { battery_key=NR; getline; match($0, "[0-9]{1,3}"); battery_value = substr($0, RSTART, RLENGTH) } /\<key\>Product\<\/key\>/ { getline; if ($0 ~ dev) print battery_value; exit 0 }'
+ powerValue=57
+ [[ -n 57 ]]
+ [[ 57 -le 20 ]]
+ for index in '${!DEVICES[*]}'
+ device='Magic Keyboard'
++ /usr/sbin/ioreg -r -l -n AppleHSBluetoothDevice -a
++ awk -v 'dev=Magic Keyboard' 'BEGIN { battery_key=0; battery_value=0 } /\<key\>BatteryPercent\<\/key\>/ { battery_key=NR; getline; match($0, "[0-9]{1,3}"); battery_value = substr($0, RSTART, RLENGTH) } /\<key\>Product\<\/key\>/ { getline; if ($0 ~ dev) print battery_value; exit 0 }'
+ powerValue=
+ [[ -n '' ]]
+ len=0
+ ((  0 > 0  ))

@thisdougb
Copy link
Author

thisdougb commented Apr 25, 2020

can you post the output of, it's probably just the wrong name I've used.

/usr/sbin/ioreg -r -l -n AppleHSBluetoothDevice -a

or via email/sms

@phips
Copy link

phips commented Apr 25, 2020

I've pruned the output (full dump is 83,000 lines; really Apple, WTF?)

iMac:~$ ioreg -r -l -n AppleHSBluetoothDevice -a | grep -iA2 product 
		<key>Bluetooth Product Name</key>
		<string>Magic Mouse 2</string>
		<key>CFBundleIdentifier</key>
--
				<key>ProductString</key>
				<string>Magic Mouse 2</string>
			</dict>
--
				<key>Bluetooth Product Name</key>
				<string>Magic Mouse 2</string>
				<key>IOObjectClass</key>
--
										<key>Product</key>
										<string>Magic Mouse 2</string>
										<key>ProductID</key>
										<integer>617</integer>
										<key>ProductIDArray</key>
										<array>
											<integer>617</integer>
--
								<key>Product</key>
								<string>Magic Mouse 2</string>
								<key>ProductID</key>
								<integer>617</integer>
								<key>ReportDescriptor</key>
--
						<key>Product</key>
						<string>Magic Mouse 2</string>
						<key>ProductID</key>
						<integer>617</integer>
						<key>ReportDescriptor</key>
--
						<key>idProductArray</key>
						<array>
							<integer>613</integer>
--
				<key>idProduct</key>
				<integer>617</integer>
				<key>idVendor</key>
--
				<key>Bluetooth Product Name</key>
				<string>Magic Mouse 2</string>
				<key>IOObjectClass</key>
--
												<key>Product</key>
												<string>Magic Mouse 2</string>
												<key>ProductID</key>
												<integer>617</integer>
												<key>ReportInterval</key>
--
										<key>Product</key>
										<string>Magic Mouse 2</string>
										<key>ProductID</key>
										<integer>617</integer>
										<key>ProductIDArray</key>
										<array>
											<integer>617</integer>
--
								<key>Product</key>
								<string>Magic Mouse 2</string>
								<key>ProductID</key>
								<integer>617</integer>
								<key>ReportDescriptor</key>
--
						<key>Product</key>
						<string>Magic Mouse 2</string>
						<key>ProductID</key>
						<integer>617</integer>
						<key>ReportDescriptor</key>
--
						<key>idProductArray</key>
						<array>
							<integer>613</integer>
--
				<key>idProduct</key>
				<integer>617</integer>
				<key>idVendor</key>
--
				<key>Bluetooth Product Name</key>
				<string>Magic Mouse 2</string>
				<key>IOObjectClass</key>
--
								<key>Product</key>
								<string>Magic Mouse 2</string>
								<key>ProductID</key>
								<integer>617</integer>
								<key>ReportDescriptor</key>
--
						<key>Product</key>
						<string>Magic Mouse 2</string>
						<key>ProductID</key>
						<integer>617</integer>
						<key>ReportDescriptor</key>
--
						<key>idProductArray</key>
						<array>
							<integer>613</integer>
--
				<key>idProduct</key>
				<integer>617</integer>
				<key>idVendor</key>
--
				<key>Product</key>
				<string>Mouse</string>
				<key>ProductID</key>
				<integer>617</integer>
				<key>ReportDescriptor</key>
--
		<key>Product</key>
		<string>Magic Mouse 2</string>
		<key>ProductID</key>
		<integer>617</integer>
		<key>ReportDescriptor</key>
--
		<key>Bluetooth Product Name</key>
		<string>Magic Keyboard</string>
		<key>CFBundleIdentifier</key>
--
				<key>ProductString</key>
				<string>Magic Keyboard</string>
			</dict>
--
				<key>Bluetooth Product Name</key>
				<string>Magic Keyboard</string>
				<key>IOObjectClass</key>
--
										<key>Product</key>
										<string>Magic Keyboard</string>
										<key>ProductID</key>
										<integer>615</integer>
										<key>ProductIDArray</key>
										<array>
											<integer>615</integer>
--
								<key>Product</key>
								<string>Magic Keyboard</string>
								<key>ProductID</key>
								<integer>615</integer>
								<key>ReportDescriptor</key>
--
						<key>Product</key>
						<string>Magic Keyboard</string>
						<key>ProductID</key>
						<integer>615</integer>
						<key>ReportDescriptor</key>
--
						<key>idProductArray</key>
						<array>
							<integer>613</integer>
--
				<key>idProduct</key>
				<integer>615</integer>
				<key>idVendor</key>
--
				<key>Bluetooth Product Name</key>
				<string>Magic Keyboard</string>
				<key>IOObjectClass</key>
--
										<key>Product</key>
										<string>Magic Keyboard</string>
										<key>ProductID</key>
										<integer>615</integer>
										<key>ProductIDArray</key>
										<array>
											<integer>615</integer>
--
								<key>Product</key>
								<string>Magic Keyboard</string>
								<key>ProductID</key>
								<integer>615</integer>
								<key>ReportDescriptor</key>
--
						<key>Product</key>
						<string>Magic Keyboard</string>
						<key>ProductID</key>
						<integer>615</integer>
						<key>ReportDescriptor</key>
--
						<key>idProductArray</key>
						<array>
							<integer>613</integer>
--
				<key>idProduct</key>
				<integer>615</integer>
				<key>idVendor</key>
--
				<key>Bluetooth Product Name</key>
				<string>Magic Keyboard</string>
				<key>IOObjectClass</key>
--
								<key>Product</key>
								<string>Magic Keyboard</string>
								<key>ProductID</key>
								<integer>615</integer>
								<key>ReportDescriptor</key>
--
						<key>Product</key>
						<string>Magic Keyboard</string>
						<key>ProductID</key>
						<integer>615</integer>
						<key>ReportDescriptor</key>
--
						<key>idProductArray</key>
						<array>
							<integer>613</integer>
--
				<key>idProduct</key>
				<integer>615</integer>
				<key>idVendor</key>
--
				<key>Product</key>
				<string>Keyboard</string>
				<key>ProductID</key>
				<integer>615</integer>
				<key>ReportDescriptor</key>
--
		<key>Product</key>
		<string>Magic Keyboard</string>
		<key>ProductID</key>
		<integer>615</integer>
		<key>ReportDescriptor</key>

@thisdougb
Copy link
Author

bugfix 😁

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