Call Parking Guide
This guide covers call parking in VueSIP using the useAmiParking composable, enabling you to build applications with call park/retrieve functionality, parking lot monitoring, and timeout handling.
Overview
Call parking allows you to put a call into a "parking lot" where it can be retrieved from any phone. This is essential for:
- Reception: Park calls for employees to pick up
- Call Centers: Queue management without traditional hold
- Multi-location: Allow call retrieval from different phones
- Busy Environments: Manage multiple calls efficiently
Why Call Parking Matters:
- Flexibility: Calls can be retrieved from any phone in the system
- Customer Experience: Better than traditional hold for transfers
- Efficiency: Visual parking lot status helps manage calls
- Team Collaboration: Share incoming calls among team members
Table of Contents
- Prerequisites
- Basic Setup
- Parking Calls
- Retrieving Calls
- Monitoring Parking Lots
- Event Handling
- Advanced Usage
- Asterisk Configuration
Prerequisites
Before using useAmiParking, ensure you have:
- AMI WebSocket Proxy: An amiws proxy running alongside Asterisk
- AMI Permissions: User with call and originate permissions
- Asterisk Parking: Configured res_parking module
- VueSIP AMI Connection: Established AmiClient connection
Basic Setup
Creating the Parking Composable
typescript
import { computed } from 'vue'
import { useAmi, useAmiParking } from 'vuesip'
// Get the AMI client
const ami = useAmi()
// Create the parking composable
const {
parkingLots, // Map of parking lot configurations
parkedCalls, // Array of all parked calls
isLoading, // Loading state
error, // Error message if any
totalParkedCalls, // Total count of parked calls
// Methods
getParkingLots, // Get all parking lot configurations
getParkedCalls, // Get currently parked calls
parkCall, // Park a call
retrieveCall, // Retrieve a parked call
refreshParkingLot, // Refresh parking lot status
getParkedCallBySpace,// Find call by parking space
onParkingEvent, // Listen for parking events
} = useAmiParking(computed(() => ami.getClient()))With Options
typescript
const parking = useAmiParking(
computed(() => ami.getClient()),
{
// Default parking lot name (default: 'default')
defaultParkingLot: 'default',
// Auto-refresh when client connects (default: true)
autoRefresh: true,
// Polling interval in ms (0 = disabled, default: 0)
pollInterval: 30000, // Refresh every 30 seconds
// Callbacks
onCallParked: (call) => {
console.log(`Call parked at ${call.parkingSpace}`)
},
onCallRetrieved: (call, retriever) => {
console.log(`Call retrieved from ${call.parkingSpace}`)
},
onCallTimeout: (call) => {
console.log(`Parked call timed out at ${call.parkingSpace}`)
},
onCallGiveUp: (call) => {
console.log(`Caller hung up while parked at ${call.parkingSpace}`)
},
// Filter parked calls
parkedCallFilter: (call) => call.parkingLot === 'sales',
// Transform parked call data
transformParkedCall: (call) => ({
...call,
displayName: formatCallerName(call),
}),
}
)Parking Calls
Basic Park Operation
typescript
// Park the current call
// Returns the parking space number
const space = await parkCall('PJSIP/1001-00000001')
console.log(`Call parked at space ${space}`)Park with Options
typescript
// Park to a specific lot
const space = await parkCall('PJSIP/1001-00000001', 'vip')
// Park with a custom timeout (in seconds)
const space = await parkCall('PJSIP/1001-00000001', 'default', 120)Using with Current Call
typescript
import { useSipClient, useAmi, useAmiParking } from 'vuesip'
const { currentCall } = useSipClient()
const ami = useAmi()
const { parkCall } = useAmiParking(computed(() => ami.getClient()))
async function parkCurrentCall() {
const call = currentCall.value
if (!call?.session) {
console.error('No active call to park')
return
}
try {
// Get the channel ID from the call session
const channelId = call.session.remote_identity?.uri?.user
const space = await parkCall(`PJSIP/${channelId}`)
console.log(`Call parked at space ${space}`)
} catch (error) {
console.error('Failed to park call:', error)
}
}Retrieving Calls
Basic Retrieve Operation
typescript
// Retrieve a call from a specific space
await retrieveCall(701, 'PJSIP/1002-00000001')Retrieve to Current Extension
typescript
async function retrieveParkedCall(space: number) {
try {
// Retrieve to the current user's channel
await retrieveCall(space, `PJSIP/${currentExtension}`)
console.log(`Retrieved call from space ${space}`)
} catch (error) {
console.error('Failed to retrieve call:', error)
}
}Retrieve from Specific Lot
typescript
// Retrieve from a specific parking lot
await retrieveCall(801, 'PJSIP/1002-00000001', 'vip')Monitoring Parking Lots
Getting Parking Lot Information
typescript
// Get all parking lots
const lots = await getParkingLots()
lots.forEach(lot => {
console.log(`Lot: ${lot.name}`)
console.log(` Spaces: ${lot.startSpace}-${lot.endSpace}`)
console.log(` Total: ${lot.totalSpaces}`)
console.log(` Timeout: ${lot.timeout} seconds`)
})ParkingLot Structure
typescript
interface ParkingLot {
name: string // Parking lot name
startSpace: number // First parking space number
endSpace: number // Last parking space number
timeout: number // Default timeout in seconds
totalSpaces: number // Total available spaces
occupied: number // Currently occupied spaces
serverId?: number // AMI server ID
}Getting Parked Calls
typescript
// Get all parked calls
const calls = await getParkedCalls()
calls.forEach(call => {
console.log(`Space ${call.parkingSpace}: ${call.callerIdNum}`)
console.log(` Parked for: ${call.duration} seconds`)
console.log(` Timeout in: ${call.timeoutRemaining} seconds`)
})
// Get calls from a specific lot
const vipCalls = await getParkedCalls('vip')ParkedCall Structure
typescript
interface ParkedCall {
parkingSpace: number // The parking space number
parkingLot: string // Parking lot name
channel: string // Channel identifier
uniqueId: string // Unique call ID
linkedId: string // Linked call ID
callerIdNum: string // Caller's number
callerIdName: string // Caller's name
timeout: number // Total timeout value
duration: number // Time parked (seconds)
parkedAt: Date // When the call was parked
serverId?: number // AMI server ID
}Finding a Specific Parked Call
typescript
// Find by space number
const call = getParkedCallBySpace(701, 'default')
if (call) {
console.log(`Found: ${call.callerIdName} (${call.callerIdNum})`)
} else {
console.log('Space 701 is empty')
}Event Handling
Listening for Parking Events
typescript
// Register a listener
const unsubscribe = onParkingEvent((event) => {
switch (event.type) {
case 'parked':
console.log(`Call parked at ${event.call.parkingSpace}`)
break
case 'retrieved':
console.log(`Call retrieved from ${event.call.parkingSpace}`)
break
case 'timeout':
console.log(`Call timed out at ${event.call.parkingSpace}`)
break
case 'giveup':
console.log(`Caller hung up at ${event.call.parkingSpace}`)
break
}
})
// Later: unsubscribe
unsubscribe()Using Callback Options
typescript
const parking = useAmiParking(clientRef, {
onCallParked: (call) => {
// Show notification
showNotification(`Call from ${call.callerIdNum} parked at ${call.parkingSpace}`)
},
onCallTimeout: (call) => {
// Alert about timeout
playAlertSound()
showWarning(`Call at ${call.parkingSpace} timed out!`)
},
})Advanced Usage
Visual Parking Lot Component
vue
<template>
<div class="parking-lot">
<h3>{{ lot.name }} Parking</h3>
<div class="parking-spaces">
<div
v-for="space in spaces"
:key="space.number"
class="parking-space"
:class="{
occupied: space.call,
warning: space.call?.timeoutRemaining < 30
}"
@click="handleSpaceClick(space)"
>
<span class="space-number">{{ space.number }}</span>
<template v-if="space.call">
<span class="caller-name">{{ space.call.callerIdName }}</span>
<span class="caller-number">{{ space.call.callerIdNum }}</span>
<span class="timer">{{ formatDuration(space.call.duration) }}</span>
</template>
<template v-else>
<span class="empty">Empty</span>
</template>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, ref, onMounted } from 'vue'
import { useAmiParking } from 'vuesip'
const props = defineProps<{
lotName: string
}>()
const {
parkingLots,
parkedCalls,
getParkingLots,
retrieveCall
} = useAmiParking(clientRef)
const lot = computed(() =>
parkingLots.value.get(props.lotName)
)
const spaces = computed(() => {
if (!lot.value) return []
const result = []
for (let i = lot.value.startSpace; i <= lot.value.endSpace; i++) {
result.push({
number: i,
call: parkedCalls.value.find(
c => c.parkingSpace === i && c.parkingLot === props.lotName
)
})
}
return result
})
async function handleSpaceClick(space: { number: number; call?: any }) {
if (space.call) {
// Retrieve the call
await retrieveCall(space.number, `PJSIP/${myExtension}`, props.lotName)
}
}
function formatDuration(seconds: number): string {
const mins = Math.floor(seconds / 60)
const secs = seconds % 60
return `${mins}:${secs.toString().padStart(2, '0')}`
}
onMounted(() => {
getParkingLots()
})
</script>
<style scoped>
.parking-spaces {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 1rem;
}
.parking-space {
padding: 1rem;
border: 2px solid var(--border-color);
border-radius: 8px;
cursor: pointer;
}
.parking-space.occupied {
background: var(--primary-light);
border-color: var(--primary);
}
.parking-space.warning {
animation: pulse 1s infinite;
border-color: var(--warning);
}
</style>Multi-Lot Dashboard
vue
<template>
<div class="parking-dashboard">
<div class="summary">
<span class="total">{{ totalParkedCalls }} calls parked</span>
</div>
<div class="lots">
<div v-for="[name, lot] in parkingLots" :key="name" class="lot-card">
<h4>{{ name }}</h4>
<div class="lot-stats">
<span>{{ lot.occupied }} / {{ lot.totalSpaces }}</span>
</div>
<div class="lot-calls">
<div
v-for="call in getCallsForLot(name)"
:key="call.uniqueId"
class="parked-call"
>
<span>{{ call.parkingSpace }}: {{ call.callerIdNum }}</span>
<button @click="retrieve(call)">Retrieve</button>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { useAmiParking } from 'vuesip'
const {
parkingLots,
parkedCalls,
totalParkedCalls,
retrieveCall
} = useAmiParking(clientRef)
function getCallsForLot(lotName: string) {
return parkedCalls.value.filter(c => c.parkingLot === lotName)
}
async function retrieve(call: ParkedCall) {
await retrieveCall(call.parkingSpace, myChannel, call.parkingLot)
}
</script>Integration with Call Controls
typescript
import { useSipClient, useAmiParking } from 'vuesip'
const { currentCall, hangup } = useSipClient()
const { parkCall, retrieveCall, parkedCalls } = useAmiParking(clientRef)
// Park current call
async function park() {
if (!currentCall.value) return
const channel = currentCall.value.session.connection?.channelId
if (channel) {
await parkCall(channel)
}
}
// Quick retrieve - get the first parked call
async function quickRetrieve() {
const firstCall = parkedCalls.value[0]
if (firstCall) {
await retrieveCall(firstCall.parkingSpace, myChannel, firstCall.parkingLot)
}
}Asterisk Configuration
res_parking.conf Setup
ini
[general]
; General parking options
[default]
; Default parking lot
parkext => 700 ; Extension to park calls (dial 700)
parkpos => 701-720 ; Parking spaces 701-720
context => parkedcalls ; Context for retrieve
parkinghints => yes ; Enable BLF for parking spaces
parkingtime => 45 ; Timeout in seconds
comebacktoorigin => yes ; Return to parker on timeout
comebackdialtime => 30 ; Ring time for callback
findslot => next ; Allocation method
[vip]
; VIP parking lot with longer timeout
parkext => 800
parkpos => 801-810
context => parkedcalls
parkingtime => 120
comebacktoorigin => no
comebackcontext => receptionDialplan for Parking
[parkedcalls]
; Retrieve parked calls
exten => _70X,1,ParkedCall(default,${EXTEN})
exten => _80X,1,ParkedCall(vip,${EXTEN})
[internal]
; Park via feature code
exten => 700,1,Park(default)
exten => 800,1,Park(vip)AMI User Permissions
ini
; manager.conf
[amiuser]
secret = your_secret
read = call,agent
write = call,originateRequired Permissions
For parking features, your AMI user needs:
read:call(for parking events)write:call(for Park action)write:originate(for retrieving calls)
Best Practices
1. Monitor Parking Lots Proactively
typescript
// Set up auto-refresh for busy environments
const parking = useAmiParking(clientRef, {
autoRefresh: true,
pollInterval: 10000, // Check every 10 seconds
})2. Handle Timeouts Gracefully
typescript
const parking = useAmiParking(clientRef, {
onCallTimeout: (call) => {
// Log the timeout
console.warn(`Call from ${call.callerIdNum} timed out`)
// Notify relevant staff
notifyReception(call)
},
})3. Show Timeout Warnings
vue
<template>
<div
class="parked-call"
:class="{ warning: isNearTimeout }"
>
<span>{{ call.callerIdNum }}</span>
<span class="countdown" v-if="isNearTimeout">
{{ timeoutRemaining }}s remaining!
</span>
</div>
</template>
<script setup lang="ts">
const isNearTimeout = computed(() =>
(props.call.timeout - props.call.duration) < 30
)
const timeoutRemaining = computed(() =>
props.call.timeout - props.call.duration
)
</script>4. Provide Visual Feedback
typescript
// Play sound when call is parked
onCallParked: (call) => {
playSound('parked')
flashParkingIndicator(call.parkingSpace)
}
// Alert when timeout is imminent
setInterval(() => {
parkedCalls.value.forEach(call => {
const remaining = call.timeout - call.duration
if (remaining === 30) {
playSound('warning')
}
})
}, 1000)