Friday, 24 February 2012

Sunset-sensitive garden lights with awk, bash and the intertubez

Update: Added code to illuminate the garden in the morning as well.

As the darkness moves later by the day, it was time to change the garden light schedule. As a reminder, my garden lights are controlled by a relay card, which is controlled by a Nanode (an Arduino with an Ethernet) which is controlled by a small Linux plug computer. The Linux box runs cron, which sends http requests over curl to the Nanode, which in its turn switches the lights on or off.

Now the easy way to change the schedule would be to edit the crontab. But that would be uncool, since darkness falls at a different time each day. The nifty way would be to have the lights on only between sunset and sleepy-time. So that's how i wanted it.

Step One: when does the sun set.

There are a few Python scripts out there which return the times of sunrise and sunset. As my tiny Linux box didn't have the proper libraries and it's running an ancient Linux (so it can't be easily updated using apt), i couldn't use that. And after all, why duplicate the effort if the effort's been made elsewhere? And when there's nothing to learn in the process?

The answer can be found at Earthtools, using an URL not quite unlike this:

http://www.earthtools.org/sun/$LAT/$LONG/$DAY/$MON/$TZ/$ISDST

To make this work, you'll need enter the values for LATitude, LONGitude, DAY, MONth, TimeZone and whether it IS DST. 

So we need to add a Step Zero, or actually a multiude of them:

  • Where are we?
  • What date is it?
  • Is it Daylight Savings Time?

The last two questions can be answered with some bash scripting, the date command and a line of awk. Note that as i am quite the awk and bash neophyte, things are probably a bit less elegant here as they would be from a seasoned shell scripter.

MON=$(date +%m)DAY=$(date +%d)TZ=$(date +%:::z)DST=$(date +%Z | grep S)ISDST=0if [ $DST ]; then  ISDST=1fi

This script fails miserably if the character S appears in your time zone, so you'll have to localize a bit before deploying, alright?

To make an educated guess as to where we are, we can either consult an on line mapping service, your friendly GPS or a web service. The web service seed like the most lazy option, and is to be found here:

http://www.geobytes.com/IpLocator.htm?GetLocation&template=php3.txt

This will return a HTML file with some funky headers containing your assumed whereabouts. It probably won't be exact enough to deploy missiles to, but good enough for a sunset value.

Now to the parsing bits, which is where the awk knowledge of the Internets way outweighs mine.

First the location. The file name of the location output above is in the variable LOCFN.

LAT=$( awk -F'"' '/name="latitude"/ {print $4}' $LOCFN)LONG=$( awk -F'"' '/name="longitude"/ {print $4}' $LOCFN)

Then the stellar bits.

sunrise=$(awk -F'[<|>]' '/sunrise/ {print $3}' $SUNFN)sunset=$(awk -F'[<|>]' '/sunset/ {print $3}' $SUNFN)ssunrise=$(date -d $sunrise +%s)ssunset=$(date -d $sunset +%s)smorning=$(date -d 06:30 +%s)snight=$(date -d 22:30 +%s)

The first two lines extract the sunrise and sunset from the file, whose name is stored in the variable SUNFN. The next two lines parse that value into seconds since The Epoch, which makes calculations doable. The last two linea are to allow calculation when the garden lights will illuminate in the morning or go out in the evening (this bit was fixed from yesterday's edition).

Like this. No special calculations are done for weekends, which is kind of lazy, but we take care of that later.

if [[ ( ( "$ssunset" -lt "$snow" ) && ( "$snow" -lt "$snight" ) )    || ( ( "$smorning" -lt "$snow" ) && ( "$snow" -lt "$ssunrise" ) ) ]] ;then        /usr/bin/curl http://relaybox/[1-4]/HIGHelse        /usr/bin/curl http://relaybox/[1-4]/LOWfi

My Nanode-connected relay box lives at the address relaybox. Or that's what i'd like you to think when you try to hack my LAN. The relays one through four are set to high if the window of darkness is upon my garden (wooh), otherwise they're drawn low.

Finally, and this is the bit i'm not entirely proud of, an edit of the crontab:

*/2 6-10 * * 1-5 /path/to/sunset/script*/2 16-22 * * 1-5 /path/to/sunset/script

This will hit the script every two minutes in the morning and evening. If the sun rises after ten-oh-oh, the lights will stay on until sixteen-hundred. It's a bug, not a feature.

Soure available upon request.

5 comments:

  1. Jag tycker ännu du borde faktorera in när det blir mörkt på er gård, man kan ju anta det finns lite träd eller hus som gör att solen inte går ner vid horisonten? Och sen ännu ta vädret i beaktande. Är det mulet, så tänder du lite tidigare.Och så kräver jag så klart ännu en webcam och publik åtkomst så att man kan leka blinkenlichts. Vem vill väl inte ha ett cafe Kauko på sin bakgård?

    ReplyDelete
  2. Jo, jag funderade faktiskt på att man borde faktorera in tex. mulenheten i ekvationen, men det får bli till version 1.2. Som du kan anta från mitt skript, så var tanken att det skulle vara portabelt. Att dessutom ingen annan än jag nödvändigtvis har REST-styrda reläer gör saken ännu nättare ;)<div> <br></div><div>Jag kan fundera på en blinkencam, men om du vill blinka våra gårdsljus får du nog komma på personligt besök. Fast nu när jag har en metod för att blinka ljus över Internätet så vore det ju skam att inte missbruka den saken på nåt sätt...<br> <br></div>

    ReplyDelete
  3. The proper way to schedule the job is at(1). Run a single cron job at a suitable time (midnight?) and obtain the correct times for switching on and switching off the lights; then schedule those as "at" jobs.For the polar regions, you need to take into account the scenario when the sun doesn't rise or set at all within the next 24 hours. (The polar circles are the boundary of the regions where the solar day exceeds 24 hours at least once per year. In practice, that means at least twice, too.)When to run the cron job is a bit of a thorny issue. Because the sun might theoretically rise before midnight (if you are not in the geographical middle of your time zone) it might be useful to run it the day before. So on October 1st you would find out when the sun rises and sets on October 2nd, and schedule the jobs for that.As for the "seasoned shell scripter", I think your code mostly looks fine. You could benefit from something like <tt>set -- $(date +"%m %d %:::z %Z")</tt> then pull out the values from $1, $2 etc, but that's a fairly minor optimization.(Famous last words: I hope I can use "tt" HTML tags in this comment. There doesn't seem to be a preview facility.)

    ReplyDelete
  4. Hello <em>e</em> and thanks for the feedback!You're very right about using <tt>at</tt> instead of using <tt>cron</tt> every two minutes. I did in fact have a reason for my madness (this time). First, the box i'm running the automation on doesn't have <tt>at</tt> and since it's running an outdated Ubuntu, i can't even update it. Something do do with the ARM platform. Yes, i should update it to Debian.The second reason is that one of the relays don't reliably transition on first <tt>wget</tt>. It's probably the power supply to my relay box which doesn't provide it with enough oomph.I'll take a look at the <tt>set --</tt> bit. It's always good to learn :). But do you have a clue on how to catch whether it is DST? Oh, and speaking of DST, it's safest to check for the current sun-times after the clock has switched to DST. Here in Finland, that's at 03:00, which either transits to 02:00 or 04:00 at the hop. So anywhere between 04:02 and the time when the lights should turn on is okay.I see that i also didn't mention that my sun-times file is saved to sun.$MON$DAY so it is only fetched once per day. After a full year, no data is wgot at all. There is probably a little glitch at leap year, but nothing worth worrying about.

    ReplyDelete
  5. The &lt;tt&gt; tags seem to work just fine.

    ReplyDelete