Become a MacRumors Supporter for $50/year with no ads, ability to filter front page stories, and private forums.

phySi0

macrumors member
Original poster
Jun 19, 2011
76
0
You don't want to know!
Code:
#!/bin/bash
#
# The computer tells the user to plug them in and shows a Growl notification every minute.
# 
# Setup:
# 1. Install growlnotify
# 2. Adjust threshold in line 20 (here 656mAh)
# 3. Create line in crontab like this:
# * * * * * /path/to/script/battery_low.sh

#Name of the user.
name="Habib"
#Level of charge in % to warn the user at.
warnat="101"

#Is the computer plugged in?
pluggedin=`/usr/sbin/system_profiler SPPowerDataType | grep "Connected:"`
#Is the computer charging?
charging=`/usr/sbin/system_profiler SPPowerDataType | grep "Charging:"`
#Is the computer fully charged?
charged=`/usr/sbin/system_profiler SPPowerDataType | grep "Fully Charged:"`

#If the computer is plugged in and charging, do nothing.
if [["$pluggedin" == *Yes* && "$charging" == *Yes*]]
then
  exit
#If the computer is plugged in and not fully charged, but not charging, tell the user.
else if [["$pluggedin" == *Yes* && "$charging" == *No* && "$charged" == *No*]]
		then
		  say "Something is wrong. Your computer is plugged in and not fully charged, but it's not charging." 
		else
			#Charge remaining in mAh.
			rem=`/usr/sbin/system_profiler SPPowerDataType | grep -i "Charge remaining" | sed 's/[^0-9]//g'`
			#Current battery capacity in mAh.
			cap=`/usr/sbin/system_profiler SPPowerDataType | grep -i "Full Charge Capacity" | sed 's/[^0-9]//g'`
			#Charge remaining in percent. -l flag makes result floating point.
			remperc=`echo "$rem/$cap*100" | bc -l`
			#This turns the value of remperc into an integer, so users don't hear a number with loads of digits after it.
			remperc=`echo "$remperc" | awk '{printf("%d\n",$1 + 0.5)}'`
			
			echo "$remperc"
			
			if [ "$remperc" -lt "$warnat" ]
				then
				#/usr/local/bin/growlnotify -n batteryAlert -p High -m "Battery level low… ($remperc%)"
				say "$name, plug me in! I'm low on juice. I only have $remperc percent left."
			fi
	fi
fi

I'm kinda late for school, but if anyone can shed any light as to what's going on here, I'd be really grateful. The problem is, the script says "Plug me in", but even if the computer is plugged in. The if statements are clearly deficient. I don't know what I'm doing wrong. It does say the correct percentage level. I put it at 101 for testing purposes. I want it to warn me at 10% or less (so warn at would be 11), but only when the computer is unplugged.

I also get this output at runtime:
Code:
/Users/Habib/Expanded/battery_low_vague.sh: line 24: [[      Connected: Yes: command not found
/Users/Habib/Expanded/battery_low_vague.sh: line 28: [[      Connected: Yes: command not found
100
The 100 is the echo, not an error statement, but the command not found. That's the same if statement that's blocking the program from working correctly.
 

phySi0

macrumors member
Original poster
Jun 19, 2011
76
0
You don't want to know!
should be
Code:
if [[ "$pluggedin" == *Yes* && "$charging" == *Yes* ]]

Thank you so much, this worked. I thought I replied already, but I guess not.

However, I am now having another problem. Launchd is giving me a massive headache. I just want to try run this script every minute, but it won't do it. I've tried running the script manually to test it and everything works as it should. Plus, I've piped the output of launchctl list to a text file and my plist is there. But it won't launch the script.

I'll show you it when I get home today.
 

phySi0

macrumors member
Original poster
Jun 19, 2011
76
0
You don't want to know!
Can you run it under crontab?

I haven't tried, but I want that to be my last resort, since launchd has superseded crontab and I prefer to do things the default way.

Edit: I'm new to all of this anyway. I use bash occasionally, but this is probably only the 2nd/3rd time shell scripting. I also am completely new to both launchd and cron. I've heard of them, but I've never used them for my own projects.
 

chown33

Moderator
Staff member
Aug 9, 2009
10,747
8,421
A sea of green
However, I am now having another problem. Launchd is giving me a massive headache. I just want to try run this script every minute, but it won't do it. I've tried running the script manually to test it and everything works as it should. Plus, I've piped the output of launchctl list to a text file and my plist is there. But it won't launch the script.

I'll show you it when I get home today.

Post the OS version you're running it on.

Post the complete actual launchd plist.

Identify exactly where the launchd plist is located. If it's in /Library (the one at the root of your hard drive, not your personal ~/Library), then it needs to have root ownership.
 

phySi0

macrumors member
Original poster
Jun 19, 2011
76
0
You don't want to know!
Post the OS version you're running it on.
OS X 10.7.3, second latest EFI update, MacBook Air Mid 2011, 13" 1.7 GHz i5.

Post the complete actual launchd plist.

This is just the current one. I've tried all sorts of combinations, including without the nice key (in fact, that's only there since I made it using Loginox and I saw I could change the nice value. None of the others had that), using Program and ProgramArgument, with PrArg Str1 as the script and the second string as start, with the second string as run, etc. This is just the latest attempt.

Code:
<?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>Label</key>
	<string>com.Habib.batteryAlert</string>
	<key>Nice</key>
	<integer>-15</integer>
	<key>ProgramArguments</key>
	<array>
		<string>/bin/sh</string>
		<string>/etc/batteryalert/batteryAlert.sh</string>
	</array>
	<key>RunAtLoad</key>
	<true/>
	<key>StartInterval</key>
	<integer>60</integer>
</dict>
</plist>

Identify exactly where the launchd plist is located.
I've tried it in LaunchAgents and LaunchDaemons in the root Library.

If it's in /Library (the one at the root of your hard drive, not your personal ~/Library), then it needs to have root ownership.
Done that already, I've set ownership to root and permissions to 644. I've also tried with ownership root and not changing permissions. Still doesn't work.
 

chown33

Moderator
Staff member
Aug 9, 2009
10,747
8,421
A sea of green
Code:
<?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>Label</key>
	<string>com.Habib.batteryAlert</string>
	<key>Nice</key>
	<integer>-15</integer>
	<key>ProgramArguments</key>
	<array>
		<string>/bin/sh</string>
		<string>/etc/batteryalert/batteryAlert.sh</string>
	</array>
[COLOR="Red"]	<key>RunAtLoad</key>
	<true/>
[/COLOR]	<key>StartInterval</key>
	<integer>60</integer>
</dict>
</plist>
Remove the RunAtLoad key. It makes no sense. From the man page for launchd.plist:
Code:
     RunAtLoad <boolean>
     This optional key is used to control whether your job is [U]launched once[/U] at
     the time the job is loaded. The default is false.  [I][emphasis added][/I]

Did you copy RunAtLoad from something else?


Also, your designated shell (/bin/sh) doesn't match the one in the first line of the shell script. I'm not saying this is the cause, just that it's an inconsistency.


Once you put the plist in place, did you use 'sudo launchctl' to actually start it? Or did you restart the computer? Because simply putting a plist in place doesn't start the job.


If you still can't make it work, isolate the problem. Make a launchd plist that launches the 'say' program every 60 secs and has it say a constant string. Now you don't have to care about your battery-checking shell script, just the launchd plist.

Finally, consider putting it in your ~/Library/LaunchAgents folder for testing.
 

phySi0

macrumors member
Original poster
Jun 19, 2011
76
0
You don't want to know!
Did you copy RunAtLoad from something else?

Yes, I was told this would make the script run when the plist is loaded, not just every * seconds. I didn't know it would mean only once. I'm not even sure the man page is saying that. It says once at startup, but subsequent times every 60 seconds should still work, I'm sure. Even so, it should at least warn me once, right? If I boot with required battery level, it should still warn me once, just not every 60 seconds.

Once you put the plist in place, did you use 'sudo launchctl' to actually start it? Or did you restart the computer? Because simply putting a plist in place doesn't start the job.

I used sudo launchctl at first, but after that, whenever I changed the plist (to fix it), I would restart. I restarted a lot of times.


If you still can't make it work, isolate the problem. Make a launchd plist that launches the 'say' program every 60 secs and has it say a constant string. Now you don't have to care about your battery-checking shell script, just the launchd plist.
Gonna try this now.

Finally, consider putting it in your ~/Library/LaunchAgents folder for testing.
Gonna try this next.
 

phySi0

macrumors member
Original poster
Jun 19, 2011
76
0
You don't want to know!
If you still can't make it work, isolate the problem. Make a launchd plist that launches the 'say' program every 60 secs and has it say a constant string. Now you don't have to care about your battery-checking shell script, just the launchd plist.

Okay, I'm going to sound like a complete troll, but when I made the "say" plist, it worked… but so did my original one. It just started working and I have no idea why. No, I didn't change anything except add the "say" plist that you suggested. I didn't remove the RunAtLoad key and they both ran at load, plus every 60 seconds, so it appears the man page is misleading.

Thanks for helping me out. Everything is fine now.
 

chown33

Moderator
Staff member
Aug 9, 2009
10,747
8,421
A sea of green
Yes, I was told this would make the script run when the plist is loaded, not just every * seconds. I didn't know it would mean only once. I'm not even sure the man page is saying that. It says once at startup, but subsequent times every 60 seconds should still work, I'm sure. Even so, it should at least warn me once, right? If I boot with required battery level, it should still warn me once, just not every 60 seconds.

Believing something is not the same as having evidence for something. And having a single piece of evidence is not the same as having repeatable evidence.

Simple logic suggests a simple strategy: try changing it, see what happens.

----------

Okay, I'm going to sound like a complete troll, but when I made the "say" plist, it worked… but so did my original one. It just started working and I have no idea why. No, I didn't change anything except add the "say" plist that you suggested. I didn't remove the RunAtLoad key and they both ran at load, plus every 60 seconds, so it appears the man page is misleading.

It's possible that something went wrong with one of the commands in your shell script. Or maybe the job-control of launchd was botched. Have you been looking at the output of launchctl list to see if the job is active, inactive, or disabled?
 

phySi0

macrumors member
Original poster
Jun 19, 2011
76
0
You don't want to know!
It's possible that something went wrong with one of the commands in your shell script. Or maybe the job-control of launchd was botched. Have you been looking at the output of launchctl list to see if the job is active, inactive, or disabled?

Yes, when it wasn't working, I piped launchctl list to a file and the plist was there, it just wasn't working. I wouldn't know how to check if it was disabled.
 

chown33

Moderator
Staff member
Aug 9, 2009
10,747
8,421
A sea of green
Yes, when it wasn't working, I piped launchctl list to a file and the plist was there, it just wasn't working. I wouldn't know how to check if it was disabled.

The columns output from launchctl list have a header. Just because the plist is there doesn't mean the job is active. Maybe use launchctl in interactive mode, and/or read its man page.

Something else: launchd reads a plist once. If you edit the file after the job is loaded, launchd won't see the edits. This is one reason there are launchctl cmds for load and unload.
 

subsonix

macrumors 68040
Feb 2, 2008
3,551
79
I haven't tried, but I want that to be my last resort, since launchd has superseded crontab and I prefer to do things the default way.

Edit: I'm new to all of this anyway. I use bash occasionally, but this is probably only the 2nd/3rd time shell scripting. I also am completely new to both launchd and cron. I've heard of them, but I've never used them for my own projects.

Just create a cron job, it's simpler and cron is ran by launchd anyway, it can thus be view as an interface to launchd, when ever cron is appropriate. In this case you want to run your script periodically, so cron should do. Launchd can do more, but is also more complex as it appears you have figured out.
 

phySi0

macrumors member
Original poster
Jun 19, 2011
76
0
You don't want to know!
Just create a cron job, it's simpler and cron is ran by launchd anyway, it can thus be view as an interface to launchd, when ever cron is appropriate. In this case you want to run your script periodically, so cron should do. Launchd can do more, but is also more complex as it appears you have figured out.

Read my post above, it's working now.
 

subsonix

macrumors 68040
Feb 2, 2008
3,551
79
Read my post above, it's working now.

Hope you are aware of the difference between agents and daemons and the different privileges and scopes that are available and that you have picked the appropriate one. It's hard to get right, you could use something like Lingon to aid you. My suggestion was based upon that you said this was your 3rd shell script ever and the KISS principle.
 

phySi0

macrumors member
Original poster
Jun 19, 2011
76
0
You don't want to know!
Hope you are aware of the difference between agents and daemons and the different privileges and scopes that are available and that you have picked the appropriate one. It's hard to get right, you could use something like Lingon to aid you. My suggestion was based upon that you said this was your 3rd shell script ever and the KISS principle.

Daemons load at system startup, agents upon any user logging in, unless it's placed in a user's Library folder, in which case it loads only when that user logs in. Am I right?

I placed it into the root Daemons folder since I want to be warned of low battery, even if I'm not logged in.
 

chown33

Moderator
Staff member
Aug 9, 2009
10,747
8,421
A sea of green
Daemons load at system startup, agents upon any user logging in, unless it's placed in a user's Library folder, in which case it loads only when that user logs in. Am I right?

Not entirely. In fact, if you're assuming that's the principal distinction, then I'd have to say "Wrong".

You really need to read TN2083:
https://developer.apple.com/library/mac/#technotes/tn2083/

In fact, you should probably bookmark it and refer to it whenever you think of writing a launchd daemon or agent.


I placed it into the root Daemons folder since I want to be warned of low battery, even if I'm not logged in.
That might be why it didn't work. Or didn't seem to work, since the only way you could tell it was working was whether it spoke something. The shell script could have been running, but because 'say' wasn't working, you heard nothing and assumed the whole shell script wasn't running.

All forms of user interaction, including sounds and speech synth, are severely restricted in a daemon. So much so you may as well assume there's no user interaction (either input or output) at all. Plus there's the whole "Whose security context is this anyway?" problem (i.e. who owns the screen interface; see TN2083).

Unless there's a specific reason that a launch agent is incapable of solving the problem, then always using a launch agent is a much better default. You should be able to fill in the blank "I absolutely can't use a launch agent because ____", and back it up with citable evidence. If that seems extreme, consider it saving yourself from daemon-induced grief.

Launch agents are allowed user interaction (though still limited). Rather than outline what's allowed and what's not, I recommend a thorough reading of TN2083.
 

phySi0

macrumors member
Original poster
Jun 19, 2011
76
0
You don't want to know!
Not entirely. In fact, if you're assuming that's the principal distinction, then I'd have to say "Wrong".

You really need to read TN2083:
https://developer.apple.com/library/mac/#technotes/tn2083/
Thank you!

In fact, you should probably bookmark it and refer to it whenever you think of writing a launchd daemon or agent.
I will.

That might be why it didn't work. Or didn't seem to work, since the only way you could tell it was working was whether it spoke something. The shell script could have been running, but because 'say' wasn't working, you heard nothing and assumed the whole shell script wasn't running.
But 'say' works absolutely fine now. I didn't move it from this folder. But that's probably why the 'growlnotify' part didn't work when I ran it via launchd as a daemon.

Based on the TN, I placed it into LaunchAgents in the root.

Here's the new version which will check the short name of the user to make the app portable:
Code:
#!/bin/bash

#Name of the user.
name=`whoami`
#Level of charge in % to warn the user at.
warnat="11"

#Is the computer plugged in?
pluggedin=`/usr/sbin/system_profiler SPPowerDataType | grep "Connected:"`
#Is the computer charging?
charging=`/usr/sbin/system_profiler SPPowerDataType | grep "Charging:"`
#Is the computer fully charged?
charged=`/usr/sbin/system_profiler SPPowerDataType | grep "Fully Charged:"`

#If the computer is plugged in and charging, do nothing.
if [[ "$pluggedin" == *Yes* && "$charging" == *Yes* ]]
then
  exit
#If the computer is plugged in and not fully charged, but not charging, tell the user.
else if [[ "$pluggedin" == *Yes* && "$charging" == *No* && "$charged" == *No* ]]
		then
		  growlnotify -m "Your battery is plugged in and not fully charged, but it's not charging." --name "Battery Alert" --title Something wrong! --image /etc/batteryalert/batteryAlert.icns --sticky --priority High
		  say -v Samantha "Something is wrong. Your computer is plugged in and not fully charged, but it's not charging."
		else
			#Charge remaining in mAh.
			rem=`/usr/sbin/system_profiler SPPowerDataType | grep -i "Charge remaining" | sed 's/[^0-9]//g'`
			#Current battery capacity in mAh.
			cap=`/usr/sbin/system_profiler SPPowerDataType | grep -i "Full Charge Capacity" | sed 's/[^0-9]//g'`
			#Charge remaining in percent. -l flag makes result floating point.
			remperc=`echo "$rem/$cap*100" | bc -l`
			#This turns the value of remperc into an integer, so users don't hear a number with loads of digits after it.
			remperc=`echo "$remperc" | awk '{printf("%d\n",$1 + 0.5)}'`
			
			if [ "$remperc" -lt "$warnat" ]
				then
				  growlnotify -m "Plug in an AC adapter now to avoid the risk of losing your work… ($remperc%)" --name "Battery Alert" --title Battery Low! --image /etc/batteryalert/batteryAlert.icns --sticky --priority High
				  say -v Samantha "$name, plug me in! I'm low on juice. I only have $remperc percent left."
			fi
	fi
fi
 

chown33

Moderator
Staff member
Aug 9, 2009
10,747
8,421
A sea of green
TBut 'say' works absolutely fine now. I didn't move it from this folder.

That's just one of the fun parts of daemonology. Sometimes they start working (or failing) for no apparent reason.

Working with daemons in the past, I've seen odd race conditions, where something didn't work simply because it happened to execute before some other daemon had a chance to, so the thing I wanted to use (network access, IIRC) failed intermittently. Moving it to be a launch agent completely solved the race condition.
 

sero

macrumors member
Aug 28, 2008
91
14
I assume this isn't working because there a number errors in the script. Adding some echo commands is a good way to test, and/or testing out specific lines directly in terminal, usually with echo or piping into more.

Your if statements will not evaluate as you expect for a few reasons. I would probably do something like this:
Code:
sysprofile_stuff=$(system_profiler SPPowerDataType)
pluggedin=$(echo "$sysprofile_stuff"|grep Connected|awk '{print $NF}')
charging=$(echo "$sysprofile_stuff"|grep Charging|head -n1|awk '{print $NF}')
charged=$(echo "$sysprofile_stuff"|grep Charged|awk '{print $NF}')

# and so on for other variables
# And as an example of why you should test your code, I get two matches
# for "Charging" - that kind of thing will bork your script if you don't know 
# about it and plan for it.

Your if statements should look something like this:
Code:
if [[ "$pluggedin" == "Yes" && "$charging" == "Yes" ]]

# you could also use awk here and not change your variables, 
# but this more efficient

Also
Code:
remperc=$(echo "$rem"/"$cap"*100 | bc -l)
remperc=$(echo "$remperc" |cut -d'.' -f1)

# or use `` to invoke subshells if you prefer, doesn't matter
# and.. these variables both have the same name, so what is
# the point of setting this to float and then back to a whole number,
# which is what your awk command was doing, BTW
 

phySi0

macrumors member
Original poster
Jun 19, 2011
76
0
You don't want to know!
I assume this isn't working because there a number errors in the script. Adding some echo commands is a good way to test, and/or testing out specific lines directly in terminal, usually with echo or piping into more.

Your if statements will not evaluate as you expect for a few reasons. I would probably do something like this:
Code:
sysprofile_stuff=$(system_profiler SPPowerDataType)
pluggedin=$(echo "$sysprofile_stuff"|grep Connected|awk '{print $NF}')
charging=$(echo "$sysprofile_stuff"|grep Charging|head -n1|awk '{print $NF}')
charged=$(echo "$sysprofile_stuff"|grep Charged|awk '{print $NF}')

# and so on for other variables
# And as an example of why you should test your code, I get two matches
# for "Charging" - that kind of thing will bork your script if you don't know 
# about it and plan for it.

Your if statements should look something like this:
Code:
if [[ "$pluggedin" == "Yes" && "$charging" == "Yes" ]]

# you could also use awk here and not change your variables, 
# but this more efficient

Also
Code:
remperc=$(echo "$rem"/"$cap"*100 | bc -l)
remperc=$(echo "$remperc" |cut -d'.' -f1)

# or use `` to invoke subshells if you prefer, doesn't matter
# and.. these variables both have the same name, so what is
# the point of setting this to float and then back to a whole number,
# which is what your awk command was doing, BTW

The script is working perfectly fine and it evaluates perfectly. I've been using like this since my last post now.

And as an example of why you should test your code

I've tested the script and it works perfectly. I don't have any issues on my side. It's more the launchd stuff that gives headaches.

However, I now have a new issue. I'm trying to make an installer package to make it easy for other users to install it, but now it's just stopped working. I removed my one and used the installer to test if it works, but it just stops.

When I check launchctl list and pipe it to a file, I search for my plist, but it isn't there. What the? I made sure all permissions were correct and placed it into /Library/LaunchAgents. I even made the installer require a restart. But no, it just won't show up.

Also, I have tried adding scripts to load the plist after installation, but that doesn't work, so I just went with restarting.

(I have also tried adding preupgrade and postupgrade scripts to unload it first, if it's already installed, then load the new one, but I can't even get it to work with a restart, so I don't want to go here yet.)

I will attach the Installer document, so if you have PackageMaker, you can see if anything's wrong. I will also attach the package itself, so Pacifist users can also see.

Sorry to bother you chown, I really didn't want to after you've helped so much already, but I've been trying to fix this for ages and it just won't work.
 

Attachments

  • batteryAlert.zip
    190.7 KB · Views: 55
Last edited:

phySi0

macrumors member
Original poster
Jun 19, 2011
76
0
You don't want to know!
Disregard!

However, I now have a new issue. I'm trying to make an installer package to make it easy for other users to install it, but now it's just stopped working. I removed my one and used the installer to test if it works, but it just stops.

Disregard. It started working after another restart, which I did for a completely unrelated reason, but meh… I'm going to need to do some more testing.
 
Register on MacRumors! This sidebar will go away, and you'll see fewer ads.