Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Script to play a warning when battery level is under a threshold in MacOS

I am using a MacBook and the battery is unreliable. I am trying to find a script command that will enable the laptop to play a beep when the battery level is lower than a threshold. After some search and tests, I found a quick solution below:

1) I first create a file called check_battery with the following contents:

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

2) I then created the following file:

\#!/bin/bash
sh check_battery>battery;
let t=60
read a < battery
if(( "$a" < "$t" ));
  then
  say "Battery level is lower than 60%!";
  say "BEAP BEAP"
fi;

3) Finally, I try to add it to my crontab job, but crontab has been stopped by iOS. Then I found that I have to use launchd, which is detailed below: https://alvinalexander.com/mac-os-x/mac-osx-startup-crontab-launchd-jobs

It is now working at my MacBook labtop but there may be better solutions. Do share with me if you have other solutions. Many thanks.

like image 376
Mike Avatar asked Jan 01 '19 17:01

Mike


1 Answers

I think your script is a good script in its own right, but could end up being quite resource-intensive when run regularly because it invokes a shell process plus two programs to acquire the information you're after.

In this instance, I think AppleScript has a couple of advantages of a shell script, largely thanks to osascript, which is the only program that needs to be executed to run an AppleScript, and it can do so without creating a shell process.

AppleScript to Retrieve Battery Info

Here's one such script that can retrieve information about your computer's battery, and issue warnings at appropriate times:

property threshold : 20 -- The level below which the script issues warnings
property verbose : false -- Report current battery level on every run
property file : "/tmp/battery.log" -- The path to the log file (from plist)


set eof of my file to 0

tell the battery()
    set warning_level to the warningLevel()
    if the warning_level = 1 then return check()
    warn about (warning_level - 1) * 5 with alert
end tell

# warn
#   Sends out a visible and audible warning with information about the
#   remaining charge (+level) on the battery.  Invoke without alert to have
#   the +level reported in the notification centre as a percentage.  Invoke 
#   with alert to have the +level reported in a pop-up alert dialog as an
#   estimation of the remaining battery time (in minutes), and which must be
#   dismissed by the user.
to warn about level without alert
    if not alert then display notification ¬
        ["Current battery level: ", level, "%"] as text ¬
        with title ["⚠️ Battery Warning"] sound name "Basso"

    say "Warning: battery low" using "Moira" pitch 127 ¬
        speaking rate 180 without waiting until completion

    if alert then display alert ["Warning: battery level is very low"] ¬
        message ["Estimated time remaining: " & level & " minutes"] ¬
        as critical giving up after 0
end warn

# battery()
#   Contains a script object that defines a number of convenience handlers that
#   retrieve information about the on-board power source
on battery()
    script
        use framework "IOKit"
        use scripting additions
        property warninglevels : ["None", "Early", "Final"]

        on warningLevel() -- A ten-minute warning indicator
            IOPSGetBatteryWarningLevel() of the current application
        end warningLevel

        on info()
            IOPSCopyPowerSourcesInfo() of the current application ¬
                as record
        end info

        to check()
            copy [it, |current capacity|, |is charging|] of ¬
                info() to [ps_info, percentage, charging]

            if the percentage ≤ threshold ¬
                and it is not charging ¬
                then warn about percentage ¬
                without alert

            if verbose then return display notification [¬
                "Percentage: ", percentage, linefeed, ¬
                "Charging: ", charging] as text ¬
                with title ["🔋 Battery Information"]

            ["Script last run on ", current date, linefeed, ¬
                linefeed, __string(ps_info), linefeed] as text
        end check
    end script
end battery

# __string()
#   A that will return an AppleScript record as a formatted string, modified
#   specifically for use in this script, therefore may not port very well to
#   handle other records from other scripts
to __string(obj)
    if class of obj = text then return obj

    try
        set E to {_:obj} as text
    on error E
        my tid:{null, "Can’t make {_:", ¬
            "} into type text.", ¬
            "{", "}", "|"}
        set E to E's text items as text
        my tid:{linefeed, ", "}
        set E to E's text items as text
    end try
end __string

# tid:
#   Set the text item delimiters
on tid:(d as list)
    local d

    if d = {} or d = {0} then set d to ""
    set my text item delimiters to d
end tid:

It retrieves most notably the current percentage of battery charge remaining and whether or not the battery is currently charging.

There are three properties at the top of the script you can safely change to your liking. In verbose mode, the battery percentage and charging status are reported every time the script is run. This will probably get irritating if you run the script minute-by-minute, so it's set to false by default.

However, when the battery level falls below the threshold, you will be alerted by a notification in the notification centre, and the soothing Scottish tones of Moira who'll provide an audible warning that the batter is low.

If the battery level becomes critically low, you'll get a pop-up alert that you will have to physically dismiss yourself.

The script also returns a bunch of other information about the battery, which generally speaking, you'll won't see. But, if the script gets executed by a launchd plist, the information returned gets logged in a log file, which you can inspect at your leisure.

launchd

launchd provides a good, fairly cost-efficient means of running scripts at regular intervals, and is much more preferable than creating a Stay Open application would be polling for the data (this is extremely inefficient, but has its uses in other scenarios).

You've already found a good link to describe the basics about writing a .plist file and how to save it, then invoke (launch) it. I'll summarise the salient points here:

  1. Create a .plist that looks something like this:

    <?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>Disabled</key>
            <false/>
            <key>Label</key>
            <string>local.battery</string>
            <key>ProgramArguments</key>
            <array>
                    <string>/usr/bin/osascript</string>
                    <string>/Users/CK/Documents/Scripts/AppleScript/scripts/Battery.applescript</string>
            </array>
            <key>RunAtLoad</key>
            <true/>
            <key>StartInterval</key>
            <integer>60</integer>
            <key>StandardErrorPath</key>
            <string>/tmp/battery.err</string>
            <key>StandardOutPath</key>
            <string>/tmp/battery.log</string>
    </dict>
    </plist>
    

    Save it with a filename that matches the <string> entry under the Label key. In this instance, I've saved mine as local.battery.plist. I've also saved my AppleScript at the path you can see specified in the plist. In my experimenting, I was quite surprised that launchd did not like it if the path had any spaces in it (even if escaped), or if I used a tilde (~) as shorthand for my home directory. Therefore, I saved the AppleScript as simply Battery.applescript and supplied the full path to where I put it.

    StartInterval is where you can specify the interval (in seconds) at which the script should be run. Personally, I think 60 seconds is a bit overkill, but I set it at that for testing. In practice, I might choose to set it at something like 600 (10 minutes), but you can judge what's best for you.

    The two /tmp/ paths at the end are where errors and return values are written to, respectively. If you change the path of the log file, don't forget to update the file property in the AppleScript.

  2. Save or move the plist to the directory ~/Library/LaunchAgents/.

  3. Load it with the following shell command from a terminal:

    launchctl load ~/Library/LaunchAgents/local.battery.plist
    
  4. If, later, you make any changes to the .plist or the .applescript, remember to unload and then re-load with launchctl:

    launchctl unload ~/Library/LaunchAgents/local.battery.plist
    launchctl load ~/Library/LaunchAgents/local.battery.plist
    

Upon successful loading of the .plist into launchd, it should run the AppleScript immediately and write the returned data to the log file. Here's what got output to my /tmp/battery.log file:

Script last run on Tuesday, 1 January 2019 at 22:01:52

is present:true
is charged:true
power source state:"AC Power"
Current:0
max capacity:100
DesignCycleCount:1000
current capacity:100
name:"InternalBattery-0"
battery provides time remaining:true
is charging:false
hardware serial number:"D8663230496GLLHBJ"
transport type:"Internal"
time to empty:0
power source id:4391011
Type:"InternalBattery"
BatteryHealth:"Check Battery"
time to full charge:0

Closing Thought

That battery information is pretty interesting the first time, but possibly quite redundant for most use cases. (That said, I've just noticed it's advised me to "Check Battery" under BatteryHealth, so I'm now quite glad I pulled the extra data).

Anyway, if the extra data isn't something you see yourself needing, I've seen the comment left by user3439894 that gives a very nice, compact, and simple way of retrieving the more salient information about the battery, i.e. it's percentage level and whether or not it's charging. This could arguably be a more appropriate AppleScripting solution than my ObjC method, which will carry a small overhead with it, plus make for a much shorter script.

For now, I'm sticking with mine, because I apparently have battery health issues and should probably get that addressed.

like image 86
CJK Avatar answered Sep 22 '22 05:09

CJK