Deeper into the Dialplan II
Using the Asterisk Database (AstDB)
Asterisk provides a powerful mechanism for storing values called the Asterisk database (AstDB). The AstDB provides a simple way to store data for use within your dialplan.
The Asterisk database stores its data in groupings called families, with values identified by keys. Within a family, a key may be used only once. For example, if we had a family called test , we could store only one value with a key called count . Each stored value must be associated with a family.
Storing Data in the AstDB
To store a new value in the Asterisk database, we use the Set() application, 5 but instead of using it to set a channel variable, we use it to set an AstDB variable. For example, to assign the count key in the test family with the value of 1 , we would write the following:
exten => 456,1,NoOp()
same => n,Set(DB(test/count)=1)
If a key named count already exists in the test family, its value will be overwritten with the new value. You can also store values from the Asterisk command line, by running the command database put <family> <key> <value> . For our example, you would type database put test count 1.
Retrieving Data from the AstDB
To retrieve a value from the Asterisk database and assign it to a variable, we use the Set() application again. Let’s retrieve the value of count (again, from the test family), assign it to a variable called COUNT , and then speak the value to the caller:
exten => 456,1,NoOp()
same => n,Set(DB(test/count)=1)
same => n,Set(COUNT=${DB(test/count)})
same => n,Answer()
same => n,SayNumber(${COUNT})
You may also check the value of a given key from the Asterisk command line by running the command database get <family> <key>. To view the entire contents of the AstDB, use the database show command.
Deleting Data from the AstDB
There are two ways to delete data from the Asterisk database. To delete a key, you can use the DB_DELETE() application. It takes the path to the key as its arguments, like this:
; deletes the key and returns its value in one step
exten => 457,1,Verbose(0, The value was ${DB_DELETE(test/count)})
You can also delete an entire key family by using the DBdeltree() application. The DBdeltree() application takes a single argument: the name of the key family to delete. To delete the entire test family, do the following:
exten => 457,1,DBdeltree(test)
To delete keys and key families from the AstDB via the command-line interface, use the database del <key> and database deltree <family> commands, respectively.
Using the AstDB in the Dialplan
There are an infinite number of ways to use the Asterisk database in a dialplan. To introduce the AstDB, we’ll look at two simple examples. The first is a simple counting example to show that the Asterisk database is persistent (meaning that it survives system reboots). In the second example, we’ll use the BLACKLIST() function to evaluate whether or not a number is on the blacklist and should be blocked.
To begin the counting example, let’s first retrieve a number (the value of the count key) from the database and assign it to a variable named COUNT . If the key doesn’t exist, DB() will return NULL (no value). Therefore, we can use the ISNULL() function to verify whether or not a value was returned. If not, we will initialize the AstDB with the Set() application, where we will set the value in the database to 1 . The next priority will send us back to priority 1 . This will happen the very first time we dial this extension:
exten =>678,1,NoOp()
same => n,Set(COUNT=${DB(test/count)})
same => n,GotoIf($[${ISNULL(${COUNT})}]?:continue)
same => n,Set(DB(test/count)=1)
same => n,Goto(1)
same=> n(continue),NoOp()
Next, we’ll say the current value of COUNT , and then increment COUNT :
exten => 678,1,NoOp()
same => n,Set(COUNT=${DB(test/count)})
same => n,GotoIf($[${ISNULL(${COUNT})}]?:continue)
same => n,Set(DB(test/count)=1)
same => n,Goto(1)
same => n(continue),NoOp()
same => n,Playback(silence/1)
same => n,SayNumber(${COUNT})
same => n,Set(COUNT=$[${COUNT} + 1])
Now that we’ve incremented COUNT , let’s put the new value back into the database. Remember that storing a value for an existing key overwrites the previous value:
exten => 678,1,NoOp()
same => n,Set(COUNT=${DB(test/count)})
same => n,GotoIf($[${ISNULL(${COUNT})}]?:continue)
same => n,Set(DB(test/count)=1)
same => n,Goto(1)
same => n(continue),NoOp()
same => n,Playback(silence/1)
same => n,SayNumber(${COUNT})
same => n,Set(COUNT=$[${COUNT} + 1])
same => n,Set(DB(test/count)=${COUNT})
Finally, we’ll loop back to the first priority. This way, the application will continue counting:
exten => 678,1,NoOp()
same => n,Set(COUNT=${DB(test/count)})
same => n,GotoIf($[${ISNULL(${COUNT})}]?:continue)
same => n,Set(DB(test/count)=1)
same => n,Goto(1)
same => n(continue),NoOp()
same => n,Playback(silence/1)
same => n,SayNumber(${COUNT})
same => n,Set(COUNT=$[${COUNT} + 1]
same => n,Set(DB(test/count)=${COUNT})
same => n,Goto(1)
Go ahead and try this example. Listen to it count for a while, and then hang up. When you dial this extension again, it should continue counting from where it left off. The value stored in the database will be persistent, even across a restart of Asterisk.
In the next example, we’ll create dialplan logic around the BLACKLIST() function, which checks to see if the current caller ID number exists in the blacklist. (The blacklist is simply a family called blacklist in the AstDB.) If BLACKLIST() finds the number in
the blacklist, it returns the value 1 ; otherwise, it will return 0 . We can use these values in combination with a GotoIf() to control whether the call will execute the Dial() application:
exten => 124,1,NoOp()
same => n,GotoIf($[${BLACKLIST()}]?blocked,1)
same => n,Dial(${JOHN})
exten => blocked,1,NoOp()
same => n,Playback(silence/1)
same => n,Playback(privacy-you-are-blacklisted)
same => n,Playback(vm-goodbye)
same => n,Hangup()
To add a number to the blacklist, run the database put blacklist <number> 1 command from the Asterisk command-line interface.
Creating a Hot-Desking Application with AstDB
With the built-in Asterisk database, you can build all sorts of applications without the need to interface with anything external. In the example below, we’ve combined all the knowledge we’ve discussed in this topic in a single set of extensions that you can include in your LocalSets context.
Hot-desking is a fairly common feature that is gathering increased traction as Asterisk systems are deployed because of the inherent flexibility the dialplan provides. Older, traditional PBX systems apply an extension number to either a line on the system or a device itself. With Asterisk, we have the ability to apply dialplan logic and information stored in a local database (or external database) to determine where an extension rings. We could easily develop a system where an extension number does nothing but ring a cell phone, or a combination of devices (like in a paging system, or a group of sales agents).
In the dialplan provided for this example of hot-desking, we’ve allowed people to log into any device by dialing 71XX where 1XX is the person’s extension number in the range 100 through 199. To log the extension out of the device, the user simply dials 7000 from the device. While it has made the dialplan and logic more complicated, the dialplan also takes into account other extensions already logged into a device someone wants to log into, and automatically logs them out first. Additionally, if we were logged into another device previously and didn’t log out before changing locations, the dialplan will log the extension out of the other device before logging it into the new location.
In order to understand the dialplan logic we’ve provided, it’s useful to see the call flow route. We’ve shown this in Figure 1.
Figure 1. Call flow for hot-desking application
[HotDesking]
; Control extension range using pattern matches
; Login with 71XX will logout existing extension at this location
; and log this device in with new extension.
; Logoff with 7000 from any device.
;
exten => 7000,1,Verbose(2,Attempting logoff from device ${CHANNEL(peername)})
same => n,Set(PeerName=${CHANNEL(peername)})
same => n,Set(CurrentExtension=${DB(HotDesk/${PeerName})})
same => n,GoSubIf($[${EXISTS(${CurrentExtension})}]?
subDeviceLogoff,1(${PeerName},${CurrentExtension}):loggedoff)
same => n,GotoIf($[${GOSUB_RETVAL} = 0]?loggedoff)
same => n,Playback(an-error-has-occurred)
same => n,Hangup()
same => n(loggedoff),Playback(silence/1&agent-loggedoff)
same => n,Hangup()
exten => _71XX,1,Verbose(2,Attempting to login device ${CHANNEL(peername)}
to extension ${EXTEN:1})
same => n,Set(NewPeerName=${CHANNEL(peername)})
same => n,Set(NewExtension=${EXTEN:1})
; Check if existing extension is logged in for this device (NewPeerName)
; — If existing extension exists (ExistingExtension)
;– get existing device name
;– If no existing device
;– (login) as we’ll overwrite existing extension for this device
;– If existing device name
;– logoff ExistingExtension + ExistingDevice
;– Goto check_device —————————————+
; — If no existing extension exists |
;– Check if existing device is logged in for this extension |
;(NewExtension) <———————————————–+
;– If existing device exists
;– Get existing extension
;– If extension exists
;– Logoff Device + Extension
;– Login
;– If no extension exists
;– Remove device from AstDB
;– Login
;– If no device exists for NewExtension
;– Login
Tests:
;* Login 101 to 0000FFFF0001 (Result: Only 101 Logged in)
;* Login 101 to 0000FFFF0002 (Result: Only 101 Logged in to new location)
;* Login 100 to 0000FFFF0001 (Result: Both 100 and 101 logged in)
;* Login 100 to 0000FFFF0002 (Result: Only 100 logged into 0000FFFF0002– Change location)
;* Login 100 to 0000FFFF0001
;* Login 100 to 0000FFFF0001 (Result: Only 100 logged in)
same => n,Set(ExistingExtension=${DB(HotDesk/${NewPeerName})})
same => n,GotoIf($[${EXISTS(${ExistingExtension})}]?get_existing_device)
same => n(check_device),NoOp()
same => n,Set(ExistingDevice=${DB(HotDesk/${NewExtension})})
same => n,GotoIf($[${EXISTS(${ExistingDevice})}]?get_existing_extension)
same => n,NoOp(Nothing to logout)
same => n,Goto(login)
same => n(get_existing_device),NoOp()
same => n,Set(ExistingDevice=${DB(HotDesk/${ExistingExtension})})
same => n,GotoIf($[${ISNULL(${ExistingDevice})}]?login)
same => n,GoSub(subDeviceLogoff,1(${ExistingDevice},${ExistingExtension}))
same => n,GotoIf($[${GOSUB_RETVAL} = 0]?check_device)
same => n,Playback(silence/1&an-error-has-occurred)
same => n,Hangup()
same => n(get_existing_extension),NoOp()
same => n,Set(ExistingExtension=${DB(HotDesk/${ExistingDevice})})
same => n,GoSubIf($[${EXISTS(${ExistingExtension})}]?
subDeviceLogoff,1(${ExistingDevice},${ExistingExtension}):remove_device)
same => n,GotoIf($[${GOSUB_RETVAL} = 0]?loggedoff)
same => n,Playback(silence/1&an-error-has-occurred)
same => n,Hangup()
same => n(remove_device),NoOp()
same => n,Set(Result=${DB_DELETE(HotDesk/${ExistingDevice})})
same => n,Goto(loggedoff)
same => n(loggedoff),Verbose(2,Existing device and extensions have
been logged off prior to login)
same => n(login),Verbose(2,Now logging in extension ${NewExtension}
to device ${NewPeerName})
same => n,GoSub(subDeviceLogin,1(${NewPeerName},${NewExtension}))
same => n,GotoIf($[${GOSUB_RETVAL} = 0]?login_ok)
same => n,Playback(silence/1&an-error-has-occurred)
same => n,Hangup()
same => n(login_ok),Playback(silence/1&agent-loginok)
same => n,Hangup()
exten => subDeviceLogoff,1,NoOp()
same => n,Set(LOCAL(PeerName)=${ARG1})
same => n,Set(LOCAL(Extension)=${ARG2})
same => n,ExecIf($[${ISNULL(${LOCAL(PeerName)})} |
${ISNULL(${LOCAL(Extension)})}]?Return(-1))
same => n,Set(PeerNameResult=${DB_DELETE(HotDesk/${LOCAL(PeerName)})})
same => n,Set(ExtensionResult=${DB_DELETE(HotDesk/${LOCAL(Extension)})})
same => n,Return(0)
exten => subDeviceLogin,1,NoOp()
same => n,Set(LOCAL(PeerName)=${ARG1})
same => n,Set(LOCAL(Extension)=${ARG2})
same => n,ExecIf($[${ISNULL(${LOCAL(PeerName)})} |
${ISNULL(${LOCAL(Extension)})}]?Return(-1))
same => n,Set(DB(HotDesk/${LOCAL(PeerName)})=${LOCAL(Extension)})
same => n,Set(DB(HotDesk/${LOCAL(Extension)})=${LOCAL(PeerName)})
same => n,Set(ReturnResult=${IF($[${DB_EXISTS(HotDesk/${LOCAL(PeerName)})}
& ${DB_EXISTS(HotDesk/${LOCAL(Extension)})}]?0:-1)})
same => n,Return(${ReturnResult})
Handy Asterisk Features
Now that we’ve gone over some more of the basics, let’s look at a few popular functions that have been incorporated into Asterisk.
Zapateller()
Zapateller() is a simple Asterisk application that plays a special information tone at the beginning of a call, which causes autodialers (usually used by telemarketers) to think that the line has been disconnected. Not only will they hang up, but their systems will flag your number as out of service, which could help you avoid all kinds of telemarketing calls. To use this functionality within your dialplan, simply call the Zapateller() application.
We’ll also use the optional nocallerid option so that the tone will be played only when there is no caller ID information on the incoming call. For example, you might use Zapateller() in the s extension of your [incoming] context, like this:
[incoming]
exten => s,1,NoOp()
same => n,Zapateller(nocallerid)
same => n,Playback(enter-ext-of-person)
Call Parking
Another handy feature is called call parking. Call parking allows you to place a call on hold in a “parking lot,” so that it can be taken off hold from another extension. Parameters for call parking (such as the extensions to use, the number of spaces, and so on)
are all controlled within the features.conf configuration file. The [general] section of the features.conf file contains four settings related to call parking:
parkext
This is the parking lot extension. Transfer a call to this extension, and the system will tell you which parking position the call is in. By default, the parking extension is 700 .
parkpos
This option defines the number of parking slots. For example, setting it to 701-720 creates 20 parking positions, numbered 701 through 720.
context
This is the name of the parking context. To be able to park calls, you must include this context.
parkingtime
If set, this option controls how long (in seconds) a call can stay in the parking lot. If the call isn’t picked up within the specified time, the extension that parked the call will be called back.
Also note that because the user needs to be able to transfer the calls to the parking lot extension, you should make sure you’re using the t and/or T options to the Dial() application. So, let’s create a simple dialplan to show off call parking:
[incoming]
include => parkedcalls
exten => 103,1,Dial(SIP/Bob,,tT)
exten => 104,1,Dial(SIP/Charlie,,tT)
To illustrate how call parking works, say that Alice calls into the system and dials extension 103 to reach Bob. After a while, Bob transfers the call to extension 700, which tells him that the call from Alice has been parked in position 701. Bob then dials Charlie
at extension 104, and tells him that Alice is at extension 701. Charlie then dials extension 701 and begins to talk to Alice. This is a simple and effective way of allowing callers to be transferred between users.
Conferencing with MeetMe()
Last but not least, let’s cover setting up an audio conference bridge with the MeetMe() application. This application allows multiple callers to converse together, as if they were all in the same physical location. Some of the main features include:
• The ability to create password-protected conferences
• Conference administration (mute conference, lock conference, or kick off participants)
• The option of muting all but one participant (useful for company announcements, broadcasts, etc.)
• Static or dynamic conference creation
Let’s walk through setting up a basic conference room. The configuration options for the MeetMe conferencing system are found in meetme.conf. Inside the configuration file, you define conference rooms and optional numeric passwords. (If a password is defined here, it will be required to enter all conferences using that room.) For our example, let’s set up a conference room at extension 600. First, we’ll set up the conference room in meetme.conf. We’ll call it 600 , and we won’t assign a password at this time:
[rooms]
conf => 600
Now that the configuration file is complete, we’ll need to restart Asterisk so that it can reread the meetme.conf file. Next, we’ll add support for the conference room to our dialplan with the MeetMe() application. MeetMe() takes three arguments: the name of the conference room (as defined in meetme.conf), a set of options, and the password the user must enter to join this conference. Let’s set up a simple conference using room 600 , the i option (which announces when people enter and exit the conference), and a password of 54321 :
exten => 600,1,MeetMe(600,i,54321)
That’s all there is to it! When callers enter extension 600 , they will be prompted for the password. If they correctly enter 54321 , they will be added to the conference. You can run core show application MeetMe from the Asterisk CLI for a list of all the options
supported by the MeetMe() application. Another useful application is MeetMeCount() . As its name suggests, this application counts the number of users in a particular conference room. It takes up to two arguments: the conference room in which to count the number of participants, and optionally a variable name to assign the count to. If the variable name is not passed as the second argument, the count is read to the caller:
exten => 601,1,NoOp()
same => n,Playback(conf-thereare)
same => n,MeetMeCount(600)
same => n,Playback(conf-peopleinconf)
If you pass a variable as the second argument to MeetMeCount() , the count is assigned to the variable, and playback of the count is skipped. You might use this to limit the number of participants, like this:
; limit the conference room to 10 participants
exten => 600,1,NoOp()
same => n,MeetMeCount(600,CONFCOUNT)
same => n,GotoIf($[${CONFCOUNT} <= 10]?meetme:conf_full,1)
same => n(meetme),MeetMe(600,i,54321)
exten => conf_full,1,Playback(conf-full)
Isn’t Asterisk fun?
Conferencing with ConfBridge()
The ConfBridge() application is the new hotness. It is essentially a replacement for the MeetMe() application for Asterisk 10 and later.
ConfBridge() was introduced using the new bridging modules that allow alternate timing sources to be used for mixing rather than only being limited to res_timing_dahdi , like MeetMe() . Additionally, several new features were added to ConfBridge() that are not available to MeetMe() such as:
• High-definition audio that can be mixed at sample rates ranging from 8 kHz to 96kHz
• Video capabilities, including the addition of dynamically switching video feeds based on loudest talker
• Dynamically controlled menu system for both conference administrators and users
• Additional options available in the confbridge.conf configuration file
We’re going to start with a basic configuration in order to get your conference bridge set up. ConfBridge() is configured via the confbridge.conf file, which contains many options, including advanced functionality for users and bridges. We’re going to start with a very basic configuration that uses the defaults. First, let’s create the de fault_user and default_bridge sections in confbridge.conf:
$ cat >> confbridge.conf
[general]
[default_user]
type=user
[default_bridge]
type=bridge
Ctrl+D
After building the confbridge.conf file, we need to load the app_confbridge.so module.
This can be done at the Asterisk console:
$ asterisk -rx “module load app_confbridge.so”
With the module loaded, we can build a simple dialplan to access our conference bridge:
[ConferenceRooms]
exten => 602,1,NoOp()
same => n,ConfBridge(${EXTEN})
[LocalSets]
include => ConferenceRooms
Of course now we need to reload our dialplan:
$ asterisk -r
*CLI> dialplan reload
If you now dial extension 602 from your phone, you should enter the conference bridge:
== Using SIP RTP CoS mark 5
— Executing [602@LocalSets:1] NoOp(“SIP/0000FFFF0001-00000001”, “”) in new
stack
— Executing [602@LocalSets:2] ConfBridge(“SIP/0000FFFF0001-00000001”, “602”)
in new stack
— <SIP/0000FFFF0001-00000001> Playing ‘conf-onlyperson.gsm’ (language ‘en’)
— <SIP/0000FFFF0001-00000001> Playing ‘confbridge-join.gsm’ (language ‘en’)
This is just the tip of the iceberg. We’ve got the base configuration done.