O'Reilly Emerging Telephony

oreilly.comSafari Books Online.Conferences.
advertisement
MySQL Conference and Expo April 14-17, 2008, Santa Clara, CA
AddThis Social Bookmark Button

Print Subscribe to Telephony Subscribe to Newsletters

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.

Pages: 1, 2

Next Pagearrow




Search Emerging Telephony

Search

Tagged Articles

Be the first to post this article to del.icio.us

Sponsored Resources

  • Inside Lightroom
Advertisement
O'reilly

© 2013, O’Reilly Media, Inc.

(707) 827-7019 (800) 889-8969

All trademarks and registered trademarks appearing on oreilly.com are the property of their respective owners.

About O'Reilly

  • Academic Solutions
  • Jobs
  • Contacts
  • Corporate Information
  • Press Room
  • Privacy Policy
  • Terms of Service
  • Writing for O'Reilly

Community

  • Authors
  • Community & Featured Users
  • Forums
  • Membership
  • Newsletters
  • O'Reilly Answers
  • RSS Feeds
  • User Groups

Partner Sites

  • makezine.com
  • makerfaire.com
  • craftzine.com
  • igniteshow.com
  • PayPal Developer Zone
  • O'Reilly Insights on Forbes.com

Shop O'Reilly

  • Customer Service
  • Contact Us
  • Shipping Information
  • Ordering & Payment
  • The O'Reilly Guarantee