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

淺唱寂寞╮ 提交于 2019-12-07 17:25:00

问题


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.


回答1:


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.




回答2:


I saw your question and tried to code something.

Here is the AppleScript:

    on run
    set theBattString to (do shell script "pmset -g batt")
    -- the above will return something like...
    -- Now drawing from 'Battery Power' -InternalBattery-0  82%; discharging; 4:06 remaining

    set batteryLevel to splitText(theBattString, space)

    --I used this line under for counting the number of items in batteryLevel. 
    --Then I get the right item for the time remaining. 
    --It is a static value for me it was item 9.

    --set totalItembatLvl to length of batteryLevel 

    tell application "System Events"
    set remainingTime to item 9 of batteryLevel

    if remainingTime < "3:10" then                 

        display alert "low battery " & remainingTime & " before shut down."


        --batteryLevel & " " & remainingTime


    end if
    end tell
    end run

    on splitText(theText, theDelimiter)
      set AppleScript's text item delimiters to theDelimiter
      set theTextItems to every text item of theText
      set AppleScript's text item delimiters to ""
      return theTextItems
    end splitText

You can save it as an idle application and it will check every minute the battery level.

    on idle
    set theBattString to (do shell script "pmset -g batt")
    -- the above will return something like...
    -- Now drawing from 'Battery Power' -InternalBattery-0  82%; discharging; 4:06 remaining

    set batteryLevel to splitText(theBattString, space)

    --set totalItembatLvl to length of batteryLevel

    set remainingTime to item 9 of batteryLevel

    if remainingTime < "2:40" then
       display alert "low battery " & remainingTime & " before shut down."

       --batteryLevel & " " & remainingTime

    end if
    return 60
    end idle

on splitText(theText, theDelimiter)
    set AppleScript's text item delimiters to theDelimiter
    set theTextItems to every text item of theText
    set AppleScript's text item delimiters to ""
    return theTextItems
end splitText

Tell me what you think, and maybe others can improve this answer.

Regards.

[EDIT]

Thanks to user @user3439894 who gives a relevant code who fits for your needs.

set batteryPercent to word 6 of paragraph 2 of (do shell script "pmset -g batt")

if batteryPercent < 40 then

    beep

    repeat 3 times

        say "Attention " & batteryPercent & "%" & " before shut down."

    end repeat

end if

Like my 2 first code you can put this one in an idle application.

 on idle

    set batteryPercent to word 6 of paragraph 2 of (do shell script "pmset -g batt")

    if batteryPercent < 40 then



        repeat 3 times

            beep

            say "Attention " & batteryPercent & "%" & " of charge before shut down."

        end repeat

    end if

    return 60

end idle

Regards



来源:https://stackoverflow.com/questions/53997451/script-to-play-a-warning-when-battery-level-is-under-a-threshold-in-macos

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!