Mimsy Were the Borogoves

Hacks: Articles about programming in Python, Perl, Swift, BASIC, and whatever else I happen to feel like hacking at.

iTunes Alarm

Jerry Stratton, August 10, 2005

Up until a few days ago I’ve had the same old annoying, buzzing alarm clock. Most mornings, the alarm will go off, I will roll over and hit the snooze button, the alarm will go off again, I will roll over and hit the snooze button... etc., until I get tired of rolling over. Then I turn the alarm clock off and go in late to work.

How wonderful it would be, I thought, to wake up to something interesting. Something that makes me want to stay awake. Something like iTunes.

Making a script that starts iTunes up at a specific time every day is pretty easy; but having done that, it seemed that it would be great to have the “iTunes Alarm” script also grab the list of the day’s events from iCal and read those out loud to me. So, this iTunes Alarm script is a bit more than iTunes. It is also a morning reminder of what I’m doing that day.

Defaults

If you’re like me, you collect a whole bunch of calendars that you really don’t want read to you every morning. The “alarmCalendars” variable lists the specific calendars whose events are important enough to read as an alarm.

  • --calendars to speak from
  • property alarmCalendars : {"Negative Space", "Writing", "Personal"}
  • --preferred sound levels
  • property systemVolume : 65
  • property iTunesVolume : 70
  • --number of seconds to gradually increase iTunes volume
  • property rampTime : 10
  • property speakingVoice : "Bruce"

Is it safe to run?

If you’re already up and listening to iTunes, there’s no need to do anything else, and in fact the last thing you’d probably want is for iTunes to suddenly go quiet and then ramp up the volume in the middle of your favorite song.

[toggle code]

  • tell application "iTunes"
    • if player state is paused or player state is stopped then
      • set computerState to "okay"
    • else
      • set computerState to "already awake"
    • end if
  • end tell
  • if computerState is "okay" then
    • --these actions will only be performed if iTunes is not already playing
  • end if

Today’s date

As far as I can tell, iCal does not have a way of asking it for all events for “today”. But we can ask it for all events between two times. Like many Unix applications, iCal and AppleScript store the date and the time of events in the same place. When we ask AppleScript for the current date, we are actually getting the current date and current time.

  • --get the start and end timestamps for today
  • set todayStart to current date
  • set todayEnd to current date
  • set time of todayStart to 0
  • set time of todayEnd to 24 * 60 * 60 - 1

Dates are useful value types in AppleScript. You can access and change parts of dates very easily. In our case, we want to set our date/time combo to the beginning of the day (12 AM) and the end of the day (11:59:59 PM). It isn’t quite that easy, however. AppleScript‘s date/time combos measure the time portion of the date/time by the number of seconds since the day started. So, 12 AM is zero seconds since the day started, and 11:59:59 PM is 24 hours times 60 minutes per hour, times 60 seconds per minute, minus 1 second. (Without subtracting the one second you might get some all-day events for the next day, since 24 full hours after 12 AM is 12 AM the next day.)

So, if this script runs at 6:05 AM on August 12th, 2005, “today” will be set to “Friday, August 12, 2005 6:05:00 AM”. After we copy that value to todayStart and set the time to 0, todayStart will be “Friday, August 12, 2005 12:00:00 AM”. After we copy today to todayEnd and set the time to 24*60*60-1, todayEnd will be “Friday, August 12, 2005 11:59:59 PM”.

Get events

Once we have the beginning and end of the day, we can ask iCal for all events that occur between those two times.

[toggle code]

  • set onVacation to false
  • set myEvents to {}
  • tell application "iCal"
    • --compile todays events from each calendar
    • set todaysEvents to {}
    • repeat with thisCalendar in alarmCalendars
      • set todaysEvents to todaysEvents & (events of calendar thisCalendar where start date ≥ todayStart and start date ≤ todayEnd)
    • end repeat
    • --get relevant information from each calendar into a list
    • repeat with anEvent in todaysEvents
      • set thisSummary to the summary of anEvent
      • if thisSummary is "Vacation" then
        • set onVacation to true
      • else
        • set thisStart to the start date of anEvent
        • set thisAllDay to the allday event of anEvent
        • set thisRecord to {eventTitle:thisSummary, startStamp:thisStart, dayEvent:thisAllDay}
        • set the end of myEvents to thisRecord
      • end if
    • end repeat
  • end tell

This section works in two passes: first, it goes through all of the calendars that we want as part of our morning announcement, and gets the events from those calendars that are happening between the beginning of today and the end of today (where start date ≥ todayStart and start date ≤ todayEnd). It puts them into a list, and it “concatenates” (appends) the new list to any previous list. The list (todaysEvents) starts out as empty because we set it to {} before talking to iCal.

The second pass goes through the list of events and gets information about the events. We create a “record” for the event that contains the name of the event, the start time, and whether or not it is an all-day event.

A “record” is a special kind of list. A record consists of “fields” and values associated with those fields. Our record ({eventTitle:thisSummary, startStamp:thisStart, dayEvent:thisAllDay}) has the fields “eventTitle”, “startStamp”, and “dayEvent”. The first will be some text (the title of our event), the second a date/time (when the event starts), and the third either true or false (true if the event is an all-day event).

We then append this record onto a list of all of the records we’ve made so far.

We could have just spoke the event information as soon as we found it, and dispensed with making lists and records. But we want to be a little smarter about when we speak the events.

One of the other things we look for in the event data is whether or not the event summary is just the one word “Vacation”. If it is, we set a variable called onVacation to true.

Call wakeup

It is now time to set off the alarm: start iTunes playing and speak the day’s events. But if we’re on vacation, we don’t want to set off the alarm. In that case, we want the script to wait until after we start using the computer before it starts iTunes and speaks the events.

[toggle code]

  • if onVacation is not true then
    • wakeUp for myEvents
  • else
    • tell application "System Events"
      • sleep
    • end tell
    • --delay a few seconds so that the rest of the script doesn't run before the computer goes to sleep
    • delay 8
    • wakeUp for myEvents
  • end if

The “neat trick” if you want to call it that, is that, if you are on vacation and want to sleep in, the script doesn’t just quit. It puts the computer to sleep, and then waits several seconds. The wait is to ensure that the rest of the script doesn’t run before the computer goes to sleep (this seems to take about two seconds on my computer).

Once you get up (sometime in the afternoon), and wake your computer up, the script continues on: it calls the handler that speaks your events for the day and starts iTunes.

The reason we made the speaking portion be a handler, is that this lets us put the wakeup part of the script in both parts of the “if onVacation is not true then” sections: both after the “if” and after the “else if”. And we don’t have to duplicate any code. If we change any part of the wakeUp handler, we need only change it once, in the wakeUp handler, instead of changing it in both the “not on vacation” section of our script and the “on vacation” section of our script.

Speak and sing!

We’re just about there. Now all we need to do is create the handler that starts iTunes and speaks the events.

[toggle code]

  • on wakeUp for theEvents
    • --first, make sure the overall system sound level is correct
    • set volume output volume systemVolume
    • --wake me gently script will go here
  • end wakeUp

This handler makes use of a prepositional parameter. The wakeup handler needs a list of events. These are the events we’re waking up for, so to speak. There are several prepositions allowed when calling a handler. Others include from, between, into, and onto.

[toggle code]

  • on getMap from myLocation between myStreets into crossStreet
    • --some stuff for mapping
  • end getMap

Before calling iTunes or speaking the events, the handler also sets the system volume to our preferred level, using the property we created back up at the top of the script.

Start iTunes

Starting iTunes is pretty simple. We already know that iTunes is in a paused or stopped state, because we checked that above. All we have to do is tell iTunes to “playpause” and it will switch from the paused/stopped state and start playing.

But if the next song happens to be, say, Alice Cooper, that could be a rude awakening. I’m creating this script to get away from loud alarms. So we are going to ramp up the sound level from practically nothing to our preferred level over a period of (in this case) ten seconds.

[toggle code]

  • tell application "iTunes"
    • set sound volume to 1
    • playpause
    • repeat with counter from 1 to rampTime
      • delay 1
      • set sound volume to iTunesVolume * counter / rampTime
    • end repeat
  • end tell

Before telling iTunes to start playing again with “playpause”, we tell it to set the sound volume to 1. This is practically silent. Then, we count up from 1 to rampTime, the property we set at the top of the script for how many seconds it should take to go to full volume.

So far, we have used “repeat” to cycle through a list of items. This time, we are using it to count up from 1 to some other number. At each part of the cycle, “counter” will contain the current count. If rampTime is 3, this repeat will repeat three times: counter will be 1, then 2, and then 3. If rampTime is 10, this repeat will repeat ten times, with counter values of from 1 to ten.

The first thing we do in this cycle is delay for one second. Once we’ve waited a second, we tell iTunes to set the sound volume to “iTunesVolume * counter / rampTime”. If rampTime is 10, this will cycle through a tenth of our preferred iTunesVolume in the first second, two tenths of our preferred iTunesVolume in the second second, up to all of our preferred iTunesVolume in the tenth second.

Speak Events

The music is playing. We are now awake, or at least awake enough to listen to our events for the day. The first thing we do is check to see if “count of theEvents” is greater than 0. That is, if there are no events, the script is done. It can end and quit.

[toggle code]

  • if the (count of theEvents) is greater than 0 then
    • delay rampTime
    • --drop the iTunes volume
    • --speak the events
    • --restore the iTunes volume
  • end if

Once we’ve waited an appropriate time (we choose to wait rampTime here, although that may or may not work for you; you may wish to create a new property for how long to wait between bringing the music up and speaking the events), we first are going to drop the iTunes volume to half, so that it doesn’t interfere with speaking the events, then we are going to speak the events, and then we are going to restore iTunes’ volume to the correct level.

First, the easy part. Controlling iTunes sound level:

[toggle code]

  • tell application "iTunes"
    • set sound volume to iTunesVolume / 2
  • end tell
  • --speaking the events will go here
  • tell application "iTunes"
    • set sound volume to iTunesVolume
  • end tell

The only thing left to do is loop through the events and speak them.

[toggle code]

  • repeat with theEvent in theEvents
    • delay 1
    • set theTitle to eventTitle of theEvent
    • say theTitle using speakingVoice
    • delay 0.5
    • if dayEvent of theEvent is not true then
      • set theStart to startStamp of theEvent
      • set startTime to the time string of theStart
      • say startTime using speakingVoice
    • else
      • say "All day" using speakingVoice
    • end if
  • end repeat

The only tricky bit here is that if this is an all-day event, we don’t speak the starting time, as that would not make sense. We speak the words “all day”.

Between each event, we wait a second. Between the event’s name and the event’s time, we wait half a second.

Tell your Macintosh when to wake up

That’s the hard part. We now have a script that can wake us up if it runs at the right time. Go ahead and turn iTunes off and run the script or double-click it in the Finder. It should start iTunes up and then speak any events occurring today.

The next part is to tell your Macintosh to wake up so that it can wake you up. You’ll want to wake your Macintosh up a few minutes before you need to wake up, so that it has time to start everything going. Under Mac OS X 10.4, you do this in your System Preferences, under the Energy Saver preferences. Click on the “Schedule...” button to bring up the sleep/wake scheduler:

Wake up weekdays: Wake up weekdays on iTunes Alarm

If you have a relatively normal work schedule where you work on weekdays, you can tell your Macintosh to wake up at a specific time every weekday. Otherwise, have it wake up every day. The next step will tell the alarm script to run on the appropriate days, letting your computer go back to sleep on its normal schedule any other day.

Tell iCal to run your alarm

Finally, now that your Macintosh is awake, you will need to tell your Macintosh to run your “iTunes Alarm”. The easiest way to do this is to use iCal. Create a repeating event that repeats weekly on Monday, Tuesday, Wednesday, Thursday, and Friday (or whenever you need to wake up).

Weekly Recurring Alarm: Weekly Recurring Alarm on iTunes Alarm

You will probably want to put this event in a calendar that is not getting read to you every morning.

The full script

[toggle code]

  • --calendars to speak from
  • property alarmCalendars : {"Negative Space", "Writing", "Personal"}
  • --preferred sound levels
  • property systemVolume : 65
  • property iTunesVolume : 70
  • --number of seconds to gradually increase iTunes volume
  • property rampTime : 10
  • property speakingVoice : "Bruce"
  • tell application "iTunes"
    • if player state is paused or player state is stopped then
      • set computerState to "okay"
    • else
      • set computerState to "already awake"
    • end if
  • end tell
  • if computerState is "okay" then
    • --get the start and end timestamps for today
    • set todayStart to current date
    • set todayEnd to current date
    • set time of todayStart to 0
    • set time of todayEnd to 24 * 60 * 60 - 1
    • --get the events from iCal and store their titles and other information into myEvents
    • set onVacation to false
    • set myEvents to {}
    • tell application "iCal"
      • --compile todays events from each calendar
      • set todaysEvents to {}
      • repeat with thisCalendar in alarmCalendars
        • set todaysEvents to todaysEvents & (events of calendar thisCalendar where start date ≥ todayStart and start date ≤ todayEnd)
      • end repeat
      • --get relevant information from each calendar into a list
      • repeat with anEvent in todaysEvents
        • set thisSummary to the summary of anEvent
        • if thisSummary is "Vacation" then
          • set onVacation to true
        • else
          • set thisStart to the start date of anEvent
          • set thisAllDay to the allday event of anEvent
          • set thisRecord to {eventTitle:thisSummary, startStamp:thisStart, dayEvent:thisAllDay}
          • set the end of myEvents to thisRecord
        • end if
      • end repeat
    • end tell
    • --if none of the events came up as a vacation, wake us up now
    • --otherwise, put the computer back to sleep and wake us up later
    • if onVacation is not true then
      • wakeUp for myEvents
    • else
      • tell application "System Events"
        • sleep
      • end tell
      • --delay a few seconds so that the rest of the script doesn't run before the computer goes to sleep
      • delay 8
      • wakeUp for myEvents
    • end if
  • end if
  • on wakeUp for theEvents
    • --first, make sure the overall system sound level is correct
    • set volume output volume systemVolume
    • --wake me gently
    • tell application "iTunes"
      • set sound volume to 1
      • playpause
      • repeat with counter from 1 to rampTime
        • delay 1
        • set sound volume to iTunesVolume * counter / rampTime
      • end repeat
    • end tell
    • --speak events
    • if the (count of theEvents) is greater than 0 then
      • delay rampTime
      • tell application "iTunes"
        • set sound volume to iTunesVolume / 2
      • end tell
      • repeat with theEvent in theEvents
        • delay 1
        • set theTitle to eventTitle of theEvent
        • say theTitle using speakingVoice
        • delay 0.5
        • if dayEvent of theEvent is not true then
          • set theStart to startStamp of theEvent
          • set startTime to the time string of theStart
          • say startTime using speakingVoice
        • else
          • say "All day" using speakingVoice
        • end if
      • end repeat
      • tell application "iTunes"
        • set sound volume to iTunesVolume
      • end tell
    • end if
  • end wakeUp

Customization

If there are regularly-known days when you will not want the alarm to go off, there are several ways you might accomplish that task. If you can put them in a special calendar in iCal, you might check that calendar, and if it contains anything at all, don’t run the script or hold it.

For example, if there are some days you don’t want the script to run at all, and those days are in a calendar called “Official Holidays”, you could add the following to the top of your script:

[toggle code]

  • --if this is an official holiday, don't run at all.
  • tell application "iCal"
    • if the (count of (events of calendar "Official Holidays" where start date ≥ todayStart and start date ≤ todayEnd)) > 0 then
      • set computerState to "official holiday"
    • end if
  • end tell

Because the script only runs the important bits if the computerState is “okay”, setting the computerState to “official holiday” means that the meat of the script will get skipped if the “Official Holidays” calendar contains anything at all.

If, on the other hand, your place of employment has a web page that announces snow days, and you want the script to treat a snow day as a vacation (that is, hold off on the alarm until you wake the computer up), you could put this snippet above the “if onVacation is not true then” line:

[toggle code]

  • --if this is a snow day, hold off on the alarm
  • --first, grab the snow day web page
  • copy (do shell script "/usr/bin/curl http://www.example.com/snowday/") to snowDay
  • --second, see if the snow day page contains the magic words
  • if snowDay contains "is a snow day" then
    • set onVacation to true
  • end if

The “do shell script” line runs a command-line utility, in this case the “curl” utility. You can get more information on it by opening terminal and typing “man curl”, but basically it gets a web page off of the net.

  1. <- Scripting Graphics
  2. AppleScript Guide ->