O'Reilly    
 Published on O'Reilly (http://oreilly.com/)
 See this if you're having trouble printing code examples


Out-Of-Office Processing with Asterisk

by Matthew Gast
09/19/2006

One of my primary motivations for setting up my own Asterisk system was my travel schedule. I had an impossibly complex series of rules that I wanted people calling me to follow about when to use my home phone, when to use my cell phone, and how to avoid disturbing me when I was far from home. With Asterisk, I could set those rules down in code and allow callers to use one number to reach me. My initial project was to enable remote SIP extensions to keep track of the local time at my remote site and avoid bothering me at inappropriate times.

My initial set of rules was not perfect, though. Because I would ring each of my Asterisk extensions in turn, there was the strong possibility that a call to my home Asterisk server would ring an extension in my home before proceeding to my remote extensions and cell phone. To speed up the process of connecting callers to me, as well as avoid unnecessary disturbances in my home when I was away, I now inform Asterisk when I will be traveling, and it handles calls accordingly.

The out-of-office processing begins when I dial a special extension and tell Asterisk the duration of my trip. By storing the time at which the trip ends in the Asterisk Database (AstDB), future calls can then be prevented from connecting to my home extension. I designed the system to store out-of-office information specific to each extension so that a collection of "follow me" extensions will not be affected.

Step 1: Get and Store the Return Time

My custom Asterisk features are activated by a "star extension," which begins with a star (*) and has a varying number of digits. To help myself remember the codes, I will usually select digits that spell out a word related to the function. Out-of-office processing is set up by calling *8747 (*TRIP).

For voice menus, I find that it is often easier to write dialplan code in the Asterisk Expression Language (AEL). My programming education emphasized validating user input, and AEL has control structures the classic dialplan language lacks that are well-suited to building voice menus that give users the opportunity to correct input. Using the AEL also allows you to follow the control flow through a dialplan much more easily.

The first step for the out-of-office setup extension is to determine where the call is coming from. My dialplan has a macro called sipfrom that will take the value of ${SIP_HEADER(from)} and extract the SIP initiator from the headers as a form of caller ID. In my setup I make a distinction between "internal" extensions that live in my home, and "remote" extensions that are SIP devices I travel with, and only internal extensions are allowed to set up this feature. For simplicity, I have also omitted a set of statements used to clarify who is making a call from the telephone wiring in my home. The phone jacks in my home support two extensions with distinctive ringing, so when a call comes in from the internal home phones, the system prompts to see which extension you are calling to set up.

_*8747 => {
    Answer;
    Set(OOO_EXTEN=${EXTEN});
    Playback(ooo/ooo-started);
    &sipfrom;
    &exten-type(${SIP_FROM});
    if ( "${EXTEN_TYPE}" : "${EXTEN_TYPE_REMOTE}" ) {
        NoOp(This only is used on internal extensions, silly!)
        Playback(ooo/internal-only);
        goto end;
    };
    NoOp(Setting OOO until for ${SIP_FROM});

The second step is to get the date that the out-of-office processing should end. The code represents the date as a six-digit string composed of two-digit substrings for the month, day, and year. However, it is also structured to add the current year on to the end of a four-digit month-and-day combination. To get the current year, the dialplan uses Asterisk's STRFTIME function, which is a gateway to the underlying operating system's function. Before using this code exactly, it is worth checking the man page strftime(3) to ensure that you have the correct specifier for the last two digits of the year.

When the processing of user input will depend on the number of digits entered, it is handy to have the switch statement in AEL. I find it especially useful to use the default option in the switch to tell the user that the input was incorrect. Due to the way that AEL is implemented, though, you cannot just go to a label in the extension from within a switch statement. The AEL parser translates the AEL dialplan into the classic dialplan, and implements switch statements as a series of gotos within a special switch-statement extension. Therefore, a goto from within a switch must specify the original extension so that it jumps back correctly. That is why the dialplan stores the extension at the start and uses the OOO_EXTEN variable as part of the goto.

I realize that by not using a four-digit year, this code is not Y3K-compliant. Even given the impressive advances in lifespan due to modern medicine, I do not expect to live to the time at which it will be a problem, because I would then be well over 100.

     enter-date:
    Read(OOO_DATE,ooo/enter-return-date,6);
    switch (${LEN(${OOO_DATE})}) {
        case 4:
            // Add current year on to end of string
            Set(CUR_YEAR=${STRFTIME(,,%g)});
            Set(OOO_DATE=${OOO_DATE}${CUR_YEAR});
            break;
        case 6:
            NoOp(Length of entered value is fine);
            break;
        case 8:
            Playback(ooo/two-digit-year-only);
            goto ${CONTEXT}|${OOO_EXTEN}|enter-date;
        default:
            Playback(ooo/date-digits-make-no-sense);
            goto ${CONTEXT}|${OOO_EXTEN}|enter-date;
    };

To validate the date input, the dial plan passes the date to the strptime(3) function, which converts a string and input descriptor into the number of epoch seconds past January 1, 1970. If it is passed a nonsensical date, such as "42/38/2006," the function will not return a time, and we can go back to the enter-date tag at the beginning of the block. If the input can be successfully converted to an epoch time, the result can be used to check that the date falls in the future.

The strptime function is available in the current Asterisk development branch, but it is not yet in the stable versions of Asterisk. Rather than update to the development code, I wrote an Asterisk Gateway Interface (AGI) wrapper that calls strptime and returns the epoch in the RESULT_EPOCH channel variable.

    Set(OOO_MONTH=${OOO_DATE:0:2});
    Set(OOO_DAY=${OOO_DATE:2:2});
    // Add back century number year
    Set(CUR_CENT=${STRFTIME(,,%C)});
    Set(OOO_YEAR=${CUR_CENT}${OOO_DATE:-2});
    Set(EXP_DATE=${OOO_YEAR}-${OOO_MONTH}-${OOO_DAY});
    AGI(agi-strptime.pl,${EXP_DATE} 00:01|%Y-%m-%d %H:%M);
    NoOp(Result is ${RESULT_EPOCH});
    if ( ${ISNULL(${RESULT_EPOCH})} ) {
        Playback(ooo/nonsensical-date);
        goto enter-date;
    };
    // Check to make sure date is in the future
    Set(NOW=${EPOCH});
    if ( ${NOW} > ${RESULT_EPOCH} ) {
        Playback(ooo/date-must-be-in-future);
        goto enter-date;
    };

As a final confirmation of the date, Asterisk can read back the date. The SayUnixTime application in Asterisk can take a format specifier; ABdY reads the date in the form "Friday, September 1, 2006."

    Playback(ooo/calls-resume-on-day);
    SayUnixTime(${RESULT_EPOCH},US/Pacific,ABdY);

With the date in hand, the dial plan proceeds to prompt for the time of day as a four-digit number. There are two shortcuts: # will stand for one minute past midnight, and * for the current time. Both are handled with a switch statement. The # key is used to terminate reading input, so pressing # will enter a zero-length string. Pressing * will have a one-character long string that is replaced with the current time. The dialplan assumes that any number entered without a leading zero is a time in the morning and prepends a zero.

     enter-time:
    Read(OOO_TIME,ooo/enter-return-time,4);
    NoOp(Entered ${OOO_TIME});
    switch (${LEN(${OOO_TIME})}) {
        case 0:
            // Set for 1 min past midnight
            Set(OOO_TIME=0001);
            break;
        case 1:
            NoOp(User entered ${OOO_TIME});
            if ( "${OOO_TIME}" = "*" ) {
                Set(CUR_HR=${STRFTIME(,US/Pacific,%H)});
                Set(CUR_MIN=${STRFTIME(,US/Pacific,%M)});
                Set(OOO_TIME=${CUR_HR}${CUR_MIN});
            } else {
                Playback(ooo/time-single-digit-bad);
                goto ${CONTEXT}|${OOO_EXTEN}|enter-time;
            };
            break;
        case 3:
            // Assume morning time
            NoOp(User entered ${OOO_TIME});
            Set(OOO_TIME=0${OOO_TIME});
        case 4:
            NoOp(right length of time entry);
            break;
        default:
            Playback(ooo/time-wrong-digits);
            goto ${CONTEXT}|${OOO_EXTEN}|enter-time;
    };

Validation of a time of day is much easier, since there are only two components with well-defined values. Therefore, the checks ensure that both the hour number and the minute number are between zero and the relevant upper bound.

    Set(OOO_MIN=${OOO_TIME:-2});
    Set(OOO_HR=${OOO_TIME:0:2});
    if ( ${OOO_HR} < 0 | ${OOO_HR} > 24 ) {
        Playback(ooo/hours-between-0-and-23);
        goto enter-time;
    };
    if ( ${OOO_MIN} < 0 | ${OOO_MIN} > 59 ) {
        Playback(ooo/minutes-between-0-and-59);
        goto enter-time;
    };

At this point, the time is valid. The epoch time of the time and day to resume normal processing is stored in AstDB. I defined the family ooo for this purpose, and store the value for each extension using that extension as a key.

    // Everything is valid, get epoch to store
    Set(EXP_DATE=${OOO_YEAR}-${OOO_MONTH}-${OOO_DAY});
    Set(EXP_TIME=${OOO_HR}:${OOO_MIN});
    AGI(agi-strptime.pl,${EXP_DATE} ${EXP_TIME}|%Y-%m-%d %H:%M);
    Set(DB(ooo/${SIP_FROM})=${RESULT_EPOCH});
    Playback(ooo/ooo-saved-in-db);
    SayUnixTime(${RESULT_EPOCH},US/Pacific);
     end:
    Hangup;
};

We now have a time to resume normal calling. Before moving on, there is one short diversion to attend to.

Step 1.5: Converting Time Strings to Epoch Time

In the previous section, I assumed an AGI function that would convert a time specification into an epoch time. The current development version of Asterisk adds the STRPTIME function as a gateway to the operating system's function of the same name, but I am not using the current development version. Rather than upgrade, I chose to write a short AGI program in Perl to handle the conversion for me.

The program assumes the time string is followed by a time-string specifier beginning with the % character. To separate the time from the specifier, it joins all the arguments together and takes everything to the left of the first % as the time. It then uses the Time::Piece function to convert the string into an epoch time. Time::Piece is smart enough to process a string with trailing white space, so there is no need to clean up after it.

# Asterisk AGI program as interface to STRPTIME() C library function
#
# Matthew Gast
#
# This program takes as arguments a time string format and a specification
# in standard C function calls, split by the pipe character ("|") and
# returns the epoch as an Asterisk channel variable

use strict;

use Asterisk::AGI;
use Time::Piece;

my $AGI = new Asterisk::AGI;
my %input = $AGI->ReadParse();

$AGI->verbose("strptime AGI converter started.\n",1);

# Create time string and specifier from the arguments
my $argumentline=join(' ',@ARGV);
my @splitargs=split(/%/,$argumentline);
my $time = $splitargs[0];
my $spec = '%' . join('%',@splitargs[1..$#splitargs]);

$AGI->verbose("Will get epoch of --$time-- with specification --$spec--\n",1);

my $t = Time::Piece->strptime($time,$spec);
my $epoch = $t->epoch;
my $offset = $t->tzoffset;

$AGI->verbose("UTC epoch value is $epoch \n",1);
$AGI->verbose("Offset to local time is $offset\n",1);
$epoch = $epoch - $offset;
$AGI->verbose ("TZ-corrected value is $epoch, setting RESULT_EPOCH\n",1);
$AGI->set_variable("RESULT_EPOCH",$epoch);

exit(0);

Step 2: Checking Out-of-Office Status

Once the epoch time for the return to the office is in AstDB, it's easy to handle. Every time an extension would be dialed, the current time should be checked against the return time. For reuse purposes, I have defined a macro named checkoutofoffice that takes the extension as an argument.

The flow through the macro is straightforward. If no record in the out-of-office database exists, the extension should ring normally. It is only when a record exists and the time is in the future that the extension should be marked as out of the office. The macro sets the SILENT_RING variable as a meta-control. Different SIP devices have different ways to suppress rings, so I use the SILENT_RING channel variable to tell the final step in connecting an extension to use the appropriate method for the SIP device in question. The macro also sets the OUTOFOFFICE variable so that rather than a silent ring, the device can be prevented from ringing at all.

macro checkoutofoffice (ext) {
// This macro checks whether or not an extension users is "out of the office,"
// and therefore, should not be rung.
//
// Input:  Extension number to check.
// Output: Sets the SILENT_RING and OUTOFOFFICE channel variables to true
//         if the user of that extension is out of the office.

Set(NAME=macro-checkoutofoffice);
NoOp(${NAME} - started);

    if ( ${DB_EXISTS(ooo/${ext})} ) {
        Gosub(db-exists);
    } else {
        NoOp(${NAME} - No OOO record found for ${ext});
    };
    goto end;

  db-exists:
       NoOp(${NAME} - OOO record found for ${ext});
    Set(OOO_UNTIL=${DB(ooo/${ext})});
    if ( ${EPOCH} < ${OOO_UNTIL} ) {
        NoOp(${NAME} - ${ext} is out of the office - no ring);
        Set(__SILENT_RING=true);
        Set(__OUTOFOFFICE=true);
    } else {
        NoOp(${NAME} - ${ext} is back in the office - ring active);
    };
    Return;

  end:
    // finish
    NoOp(${NAME} - ended);
};

Any extension that should have out-of-office processing then needs to call the out-of-office check before connecting the call. A very simple example would be the following extension code, which checks the out-of-office status and prevents connecting the call if the extension is marked as away:

1001 => {
    &checkoutofoffice(${EXTEN});
    if (${ISNULL(${OUTOFOFFICE})}) {
        // continue to connect call
        Dial(SIP/1001,20);
    } else {
        NoOp(Extension ${EXTEN} is out of the office, no connection attempt);
    };
    Voicemail(u1001);
};

My Asterisk system implements a "follow-me" system that rings multiple extensions in turn. One of the advantages to skipping over a home extension when I am out of the office is that callers will more quickly reach me.

Matthew Gast is the director of product management at Aerohive Networks responsible for the software that powers Aerohive's networking devices.


Return to O'Reilly Emerging Telephony.

Copyright © 2009 O'Reilly Media, Inc.