问题
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:
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 theLabel
key. In this instance, I've saved mine aslocal.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 thatlaunchd
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 simplyBattery.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 thefile
property in the AppleScript.Save or move the plist to the directory
~/Library/LaunchAgents/
.Load it with the following shell command from a terminal:
launchctl load ~/Library/LaunchAgents/local.battery.plist
If, later, you make any changes to the
.plist
or the.applescript
, remember to unload and then re-load withlaunchctl
: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