Bluetooth battery early warning system
The notification from macOS that a mouse battery is running low usually comes too late and at inopportune times. When it happens I soon need to stop everything or get a USB mouse somewhere.
What I usually end up doing is taking an unplanned break for a few minutes to get the mouse battery back up to a safe level, and then go back to work. And forget to plug it in for the long term when I’m done working.
It would be nice if I could customize the warning to happen earlier and be both obvious and unobtrusive when it does happen. GeekTool is perfect for obvious and unobtrusive. All I need is a script to generate the text to display and it will show up on the desktop, and only when it needs to.
If you always want to see the battery level of all battery-operated devices, use ~/bin/batteries
. In GeekTool (or on the command line) you can add the --colors
switch to display different levels with different colors; there is a warning level set to yellow, a critical level set to red, and anything else is green.
- use Term::ANSIColor;
- $normal = color 'green';
- $warning = color 'yellow';
- $critical = color 'red';
- $clear = color 'reset';
- $warningLevel = 30;
- $criticalLevel = $warningLevel/2;
I’ve set the warnings to start at 30%, with critical explicitly at half that. I may change my mind later.
Using the actual color names—green, yellow, and red—requires that you have Term::ANSIColor installed. If you don’t, you can replace those codes with the actual color escape sequences:
- $normal = "\e[32m"; #green
- $warning ="\e[33m"; #yellow
- $critical = "\e[31m"; #red
- $clear = "\e[0m";
- $warningLevel = 30;
- $criticalLevel = $warningLevel/2;
If you only want to see the battery level of devices at or below the warning level, use ~/bin/batteries --warnings
with or without --colors
.
If you want to see devices that are charging, even if they’re above the warning level, add --wired
to the command line.
The options are detected using a simple while
loop.
[toggle code]
- #check for command-line switches
-
while ($option = shift) {
-
if ($option =~ /^--colors$/) {
- $colors = 1;
-
} elsif ($option =~ /^--warnings$/) {
- $warningsOnly = 1;
-
} elsif ($option =~ /^--wired$/) {
- $showWired = 1;
-
} elsif ($option =~ /^--help$/) {
- help();
-
} elsif ($option =~ /^--verbose$/) {
- $verbose = 1;
-
} else {
- help("Unknown option $option");
- }
-
if ($option =~ /^--colors$/) {
- }
If you wanted, you could add your own abbreviations for those command-line switches, something like
- if ($option =~ /^-(c|-colors)$/) {
This would make both -c
and --colors
count as the command-line switch for using ANSI color.
As you can see, I’ve also set up a --verbose
switch. This was to show where the device name came from when testing. I’m assuming that if the device name came from Bluetooth Product Name, it is on bluetooth, and if the device name came from Product, then it is not on bluetooth. From testing, it appears that all devices have a Product name; only devices connected via Bluetooth have a Bluetooth Product Name.
What this means is that if a device has a Product name but not a Bluetooth Product Name, it is wired in. And the main reason for wiring a battery-operated device in is to charge the battery. By using --wired
along with --warnings
, the script will continue to display devices that are charging, giving you visual feedback on when it’s okay to disconnect them. Thus, the full script that I use in GeekTool is
- ~/bin/batteries --warnings --wired --colors
What if you aren’t using GeekTool but still want to know when a battery is getting low? Well, with the notification script from 42 Astounding Scripts, you can do that easily in Perl.
[toggle code]
- #!/usr/bin/perl
- #notify when a battery level is low.
- #Jerry Stratton astoundingscripts.com
-
if ($notification = `~/bin/batteries --warnings`) {
- `~/bin/notify '$notification'`;
- }
If you don’t have 42 Astounding Scripts you should buy it. In the meantime, you can call osascript
directly to get the job done.
[toggle code]
- #!/usr/bin/perl
- #notify when a battery level is low.
- #Jerry Stratton astoundingscripts.com
-
if ($notification = `~/bin/batteries --warnings`) {
- $notification =~ s/\n/\\n/g;
- `/usr/bin/osascript -e "display notification \\"$notification\\" sound name \\"beep\\""`;
- }
Save this as something like batteryWarning
and you can then enter this script into either your crontab file or into your launchd settings so that it runs regularly. For launchd, I recommend Lingon X. Lingon X can also be used to make editing your crontab file easier if you’re not comfortable using crontab -e
. Either way, something like this in the crontab file will do it:
[toggle code]
- 0 * * * * $HOME/bin/batteryWarning
This will run the script every hour on the hour, generating a warning if any device’s battery percentage is lower than the warning level.
The batteries
script works by first collecting battery data, and then displaying it. These two sections of the script are marked by comments.
[toggle code]
- #collect battery levels
- @ioText = `/usr/sbin/ioreg -l`;
-
for (@ioText) {
- $bluetoothName = $1 if m/"Bluetooth Product Name" = "([^"]*)"/;
- $wiredName = $1 if m/"Product" = "([^"]*)"/;
-
if (m/"BatteryPercent" = ([0-9]+)/) {
- my $level = $1;
- my $device = 'Unknown Device';
- my $source = 'No name found';
-
if ($bluetoothName ne '') {
- $device = $bluetoothName;
- $source = 'bluetooth';
-
} elsif ($wiredName) {
- $device = $wiredName;
- $source = 'wired';
- }
- $devices{$device} = $level;
- $sources{$device} = $source;
- $maxDeviceWidth = length($device) if length($device) > $maxDeviceWidth;
- $maxLevelWidth = length($level) if length($level) > $maxLevelWidth;
- undef $bluetoothName;
- undef $wiredName;
- }
- }
Collecting the data is literally a matter of using regular expressions to detect a product name and remember it, and then every time a battery level is detected, use the most recently-detected product name for that battery level. The bluetooth name takes precedence over the general name.
The collection loop also keeps track of the longest product name and the longest battery level, so that the two can be aligned correctly in the display loop.
[toggle code]
- #display battery levels
-
for $device (sort keys %devices) {
- $level = $devices{$device};
- $source = $sources{$device};
- next if $warningsOnly && $level > $warningLevel && (!$showWired || $source ne 'wired');
- $spaces = 1;
- $spaces += $maxDeviceWidth-length($device);
- $spaces += $maxLevelWidth-length($level);
- $alignment = ' ' x $spaces;
-
if ($colors) {
-
if ($level <= $criticalLevel) {
- print $critical;
-
} elsif ($level <= $warningLevel) {
- print $warning;
-
} else {
- print $normal;
- }
-
if ($level <= $criticalLevel) {
- }
- print "$device:$alignment$level";
- print " ($source)" if $verbose;
- print $clear if $colors;
- print "\n";
- }
The display loop sorts the device names alphabetically and displays each device with its battery percent. If --warnings
is set, the loop skips any devices that are above the warning level and (if --wired
is switched on) do not seem to be charging.
Here’s the full code:
[toggle code]
- #!/usr/bin/perl
- # show battery level for bluetooth devices
- # Jerry Stratton astoundingscripts.com
- use Term::ANSIColor;
- $normal = color 'green';
- $warning = color 'yellow';
- $critical = color 'red';
- $clear = color 'reset';
- $warningLevel = 30;
- $criticalLevel = $warningLevel/2;
- #check for command-line switches
-
while ($option = shift) {
-
if ($option =~ /^--colors$/) {
- $colors = 1;
-
} elsif ($option =~ /^--warnings$/) {
- $warningsOnly = 1;
-
} elsif ($option =~ /^--wired$/) {
- $showWired = 1;
-
} elsif ($option =~ /^--help$/) {
- help();
-
} elsif ($option =~ /^--verbose$/) {
- $verbose = 1;
-
} else {
- help("Unknown option $option");
- }
-
if ($option =~ /^--colors$/) {
- }
- #collect battery levels
- @ioText = `/usr/sbin/ioreg -l`;
-
for (@ioText) {
- $bluetoothName = $1 if m/"Bluetooth Product Name" = "([^"]*)"/;
- $wiredName = $1 if m/"Product" = "([^"]*)"/;
-
if (m/"BatteryPercent" = ([0-9]+)/) {
- my $level = $1;
- my $device = 'Unknown Device';
- my $source = 'No name found';
-
if ($bluetoothName ne '') {
- $device = $bluetoothName;
- $source = 'bluetooth';
-
} elsif ($wiredName) {
- $device = $wiredName;
- $source = 'wired';
- }
- $devices{$device} = $level;
- $sources{$device} = $source;
- $maxDeviceWidth = length($device) if length($device) > $maxDeviceWidth;
- $maxLevelWidth = length($level) if length($level) > $maxLevelWidth;
- undef $bluetoothName;
- undef $wiredName;
- }
- }
- #display battery levels
-
for $device (sort keys %devices) {
- $level = $devices{$device};
- $source = $sources{$device};
- next if $warningsOnly && $level > $warningLevel && (!$showWired || $source ne 'wired');
- $spaces = 1;
- $spaces += $maxDeviceWidth-length($device);
- $spaces += $maxLevelWidth-length($level);
- $alignment = ' ' x $spaces;
-
if ($colors) {
-
if ($level <= $criticalLevel) {
- print $critical;
-
} elsif ($level <= $warningLevel) {
- print $warning;
-
} else {
- print $normal;
- }
-
if ($level <= $criticalLevel) {
- }
- print "$device:$alignment$level";
- print " ($source)" if $verbose;
- print $clear if $colors;
- print "\n";
- }
-
sub help {
- my $message = shift;
- print "$0 [--warnings]\n";
- print "\tshow battery levels of any product with a battery level.\n";
- print "\t--colors:\tdisplay different battery warning levels in different colors.\n";
- print "\t--verbose:\tdisplay where the device name came from.\n";
- print "\t--warnings:\tonly show levels at warning level ($warningLevel%) or below.\n";
- print "\t--wired:\tshow all wired devices, even if above the warning level.\n";
- print "\n";
- print "$message.\n" if $message;
- exit;
- }
You can use the edit script from Astounding Scripts to enter that into your ~/bin
directory. Or, you can also download the full script (Zip file, 1.6 KB). If you don’t have Term::ANSIColor
and don’t want to install it, you’ll need to replace the color names with the raw escape sequences as described above. You can also, of course, adjust the warning level as you wish by altering $warningLevel
. My guess is that for most people a value between 30 and 40 will be most useful.
Download
- Bluetooth battery warning script (Zip file, 1.6 KB)
- A Perl script to help notify you when a Bluetooth device’s battery is running low.
ANSI escape sequences
- ANSI escape code
- “ANSI escape sequences are used to control text formatting, color, and other output options on text terminals.”
- Term::ANSIColor
- “Color screen output using ANSI escape sequences… This module has two interfaces, one through color() and colored() and the other through constants. It also offers the utility functions uncolor(), colorstrip(), and colorvalid(), which have to be explicitly imported to be used.”
GeekTool
- GeekTool
- “GeekTool is a System Preferences module for Mac OS 10.5. It lets you display on your desktop different kind of informations, provided by 3 default plugins.” The plugins let you monitor files (such as error logs), view images (such as live graphs), and display the results of command-line scripts.
- GeekTool, Perl, and ANSI color codes
- GeekTool is a great way to display the results of little scripts on your desktop. Using ANSI color codes can make those scripts even more useful. You can also change the status of the status button from “success” to “failure” depending on your script’s exit code.
scripting tools
- 42 Astoundingly Useful Scripts and Automations for the Macintosh
- MacOS uses Perl, Python, AppleScript, and Automator and you can write scripts in all of these. Build a talking alarm. Roll dice. Preflight your social media comments. Play music and create ASCII art. Get your retro on and bring your Macintosh into the world of tomorrow with 42 Astoundingly Useful Scripts and Automations for the Macintosh!
- Edit (Zsh)
- One of the first scripts in the book is a script to edit scripts. But that elicits a bootstrapping problem. Without the edit script, you can’t use the edit script to edit the edit script!
- Lingon: Peter Borg
- “An easy to use yet powerful app to run things automatically.”
More batteries
- What will a useful electric car look like?
- New technologies win by being better than previous technologies, not worse. The modern battery-powered car is ridiculously close to its turn of the century counterpart. They may look more modern, but the same hundred-year-old technology that has never met the needs of drivers still powers them and hobbles them.
More cron
- Catalina vs. Mojave for Scripters
- More detail about the issues I ran into updating the scripts from 42 Astounding Scripts for Catalina.
More GeekTool
- icalBuddy and eventsFrom/to
- Ali Rantakari’s icalBuddy has an error in the documentation for the “eventsFrom/to” command-line option. Rather than “tomorrow at time” use “time tomorrow”.
- Put a relative clock on your Desktop with GeekTool
- There are a lot of desktop clocks that show the absolute time. But sometimes you just want to know if the time is today, or yesterday, or two days ago. Here’s how to do it with Python and GeekTool.
- Apple Mail on the Desktop with GeekTool
- Here’s a simple AppleScript to use with GeekTool to put your inbox on the Desktop.
- GeekTool, TaskPaper, and XML
- A script to convert a TaskPaper file to XML so as to filter it for specific tags and display the results on the Desktop.
- GeekTool, Perl, and ANSI color codes
- GeekTool is a great way to display the results of little scripts on your desktop. Using ANSI color codes can make those scripts even more useful. You can also change the status of the status button from “success” to “failure” depending on your script’s exit code.
- One more page with the topic GeekTool, and other related pages