Implementación completa del sistema de colas con IVR, grabación automática, gestión de agentes y monitoreo en tiempo real
⏱️ Información del Taller
Duración: 120 minutos | Dificultad: Avanzado | Modalidad: Práctica
🎯 Objetivo de la Parte 2
Completar el sistema de central telefónica implementando el contexto de entrada , manejo de colas con grabación automática , códigos de funciones para agentes y realizar pruebas exhaustivas del sistema completo.
En esta segunda parte completaremos el sistema con:
✅ Contexto de ruta entrante que simula llamadas externas
✅ Handler de colas con grabación automática MixMonitor
✅ Sistema de login/logout para agentes (*71-*76)
✅ Códigos de consulta de estado de colas (*77)
✅ Organización automática de grabaciones por fecha
✅ Integración completa con CDR para reportes
✅ Pruebas exhaustivas del flujo completo
Este contexto simula una llamada que llega desde el exterior (como un troncal SIP).
Agregar en extensions_entradas.conf:
; ============================================================================
; CONTEXTO: Ruta Entrante (Simula llamada externa)
; ============================================================================
; Este contexto simula una llamada que llega desde un troncal SIP
; En producción, aquí llegarían las llamadas del proveedor VoIP
exten => _X.,1,NoOp(=== Llamada Entrante: ${CALLERID(num)} ===)
same => n,Set(CALLERID(name)=Cliente)
; Validar horario de atención
same => n,Gosub(func-apply-open-hours,s,1)
same => n,NoOp(Estado Horario: ${HORARIO_STATUS})
; Si está cerrado, reproducir mensaje y colgar
same => n,GotoIf($[ "${HORARIO_STATUS}" = "CLOSED" ]?fuera_horario)
; Si está abierto, ir al IVR
same => n,Goto(ivr-principal,s,1)
; === FUERA DE HORARIO ===
same => n(fuera_horario),NoOp(=== Fuera de Horario ===)
same => n,Playback(custom/fuera-horario) ; "Nuestro horario es..."
same => n,Playback(vm-goodbye)
💡 Cómo Probar
Para simular una llamada entrante desde una extensión interna editar el archivo extensions_funciones.conf:
; En extensions_funciones.conf
exten => 77777,1,NoOp(=== Simular Llamada Entrante ===)
same => n,Goto(from-pstn,123456789,1)
No te olvides de editar el extensions_categorias.conf para agregar la función:..
[cat1] ; Categoría con todos los permisos
include => anexos-internos ; Ya existe desde talleres previos
include => funciones-variadas ; Funciones varias personalizadas
include => funciones-voicemail ; Ya existe desde Taller 6
include => func-grabar-audios ; Nueva función de grabación de audios IVR
include => salidas-celular ; Ya existe desde Taller 5
[cat2] ; Categoría solo con permisos internos y colas
include => anexos-internos
include => funciones-variadas ; Funciones varias personalizadas
include => funciones-voicemail
include => func-grabar-audios ; Nueva función de grabación de audios IVR
include => salidas-celular
[cat3] ; Categoría básica
include => anexos-internos
include => funciones-variadas ; Funciones varias personalizadas
include => funciones-voicemail
include => func-grabar-audios ; Nueva función de grabación de audios IVR
include => salidas-celular
Luego marca 77777 desde cualquier extensión para probar el flujo completo.
Este contexto reemplaza el Queue() básico de la Parte 1 con una implementación profesional usando handlers b y B de Asterisk 22.
⚠️ Importante: Reemplazo del IVR de la Parte 1
En la Parte 1 , el IVR llamaba directamente a Queue() de forma básica:
same => n,Queue(${QUEUE_NAME},tT,,,300) ; Sin grabación
En la Parte 2 , reemplazamos esto con un contexto [cola-handler] profesional que:
✅ Usa handlers b para grabación automática
✅ Implementa funciones auxiliares con Gosub()
✅ Maneja estados de cola profesionalmente
✅ Integra con PostgreSQL
Acción requerida: Actualizar el IVR de la Parte 1 para que use este handler (se explica más adelante).
💡 Handlers b/B en Asterisk 22
Ventaja sobre MixMonitor manual:
Handler b : Se ejecuta en el canal del agente ANTES de conectar
Handler B : Se ejecuta en el canal del llamante ANTES de conectar
Resultado : Grabación inicia solo cuando agente contesta , no durante MOH
Beneficio : Ahorra espacio y facilita auditoría (solo llamadas atendidas)
Agregar en extensions.conf o extensions_entradas.conf:
; ============================================================================
; CONTEXTO: Handler Pre-Bridge para Grabación Automática
; ============================================================================
; Se ejecuta automáticamente cuando un agente contesta
; Aprovecha los handlers 'b' de Queue() en Asterisk 22
exten => s,1,NoOp(=== Pre-Bridge: Iniciando Grabación ===)
same => n,Set( QUEUE_AGENT =${MEMBERINTERFACE})
same => n,Set( QUEUE_CALLER =${CALLERID(num)})
same => n,Set( TIMESTAMP =${STRFTIME(${EPOCH},,%Y%m%d-%H%M%S)})
; Construir ruta YYYY/MM/DD dentro de la carpeta de la cola
same => n,Set( RECORDING_ROOT =/var/spool/asterisk/monitor)
same => n,Set( RECORDING_SUBDIR =${STRFTIME(${EPOCH},,%Y/%m/%d)})
same => n,Set( RECORDING_PATH =${RECORDING_ROOT}/${QUEUE_DEPT}/${RECORDING_SUBDIR})
same => n,System(/usr/bin/env mkdir -p ${RECORDING_PATH})
; Generar nombre de archivo con datos del CDR
same => n,Set( RECORDING_FILE =queue-${QUEUE_DEPT}-${QUEUE_CALLER}-${TIMESTAMP})
same => n,Set( RECORDING_FULLPATH =${RECORDING_PATH}/${RECORDING_FILE}.wav)
; Registrar handler de hangup para detener MixMonitor al finalizar
same => n,Set(CHANNEL(hangup_handler_push)=queue-hangup,s,1(${RECORDING_FULLPATH}))
; Iniciar grabación con MixMonitor (solo cuando agente contesta)
same => n,MixMonitor(${RECORDING_FULLPATH},b)
; Guardar en CDR para integración con PostgreSQL (Taller 8)
same => n,Set(CDR(userfield)=${RECORDING_FULLPATH})
same => n,Set(CDR(accountcode)=QUEUE-${QUEUE_NAME})
same => n,NoOp(Grabación iniciada: ${RECORDING_FULLPATH})
; ============================================================================
; CONTEXTO: Handler de Hangup para colas
; ==========================================================================
; Se ejecuta en el canal del agente cuando termina la llamada
exten => s,1,NoOp(=== Hangup: Finalizando Grabación ===)
same => n,StopMixMonitor()
same => n,NoOp(Grabación finalizada - Status: ${DIALSTATUS} - Archivo: ${ARG1})
; ============================================================================
; CONTEXTO: Handler de Colas con Gosub
; ============================================================================
; Usa handlers profesionales b/B de Queue()
exten => ventas,1,NoOp(=== Entrada a Cola Ventas ===)
same => n,Set( __QUEUE_NAME =cola-ventas)
same => n,Set( __QUEUE_DEPT =Ventas)
same => n,Gosub(func-setup-queue-call,s,1)
; Queue con handler b() para pre-bridge en el canal del agente
same => n,Queue(${QUEUE_NAME},tTb(queue-pre-bridge^s^1))
same => n,Gosub(func-handle-queue-exit,s,1)
; === COLA DE SOPORTE ===
exten => soporte,1,NoOp(=== Entrada a Cola Soporte ===)
same => n,Set( __QUEUE_NAME =cola-soporte)
same => n,Set( __QUEUE_DEPT =Soporte)
same => n,Gosub(func-setup-queue-call,s,1)
; Queue con handler b() para pre-bridge en el canal del agente
same => n,Queue(${QUEUE_NAME},tTb(queue-pre-bridge^s^1))
same => n,Gosub(func-handle-queue-exit,s,1)
; ============================================================================
; FUNCIONES AUXILIARES PARA COLAS
; ============================================================================
exten => s,1,NoOp(=== Configurando llamada de cola ===)
same => n,Set( __QUEUE_START_TIME =${EPOCH})
same => n,Set( __QUEUE_CALLER =${CALLERID(num)})
exten => s,1,NoOp(=== Manejando salida de cola: ${QUEUESTATUS} ===)
same => n,GotoIf($[ "${QUEUESTATUS}" = "TIMEOUT" ]?timeout)
same => n,GotoIf($[ "${QUEUESTATUS}" = "FULL" ]?full)
same => n,GotoIf($[ "${QUEUESTATUS}" = "JOINEMPTY" ]?no-agents)
same => n,GotoIf($[ "${QUEUESTATUS}" = "LEAVEEMPTY" ]?no-agents)
same => n(timeout),Playback(queue-timeout)
same => n(full),Playback(queue-full)
same => n(no-agents),Playback(vm-nobodyavail)
📝 Explicación de MixMonitor
MixMonitor(filename,options)
filename: Ruta completa del archivo de grabación
b: Opción para grabar solo cuando se conecta con un agente (no graba MOH)
Estructura de directorios:
/var/spool/asterisk/monitor/
│ │ │ │ ├── queue-Ventas-987654321-20251025-143022.wav
│ │ │ │ ├── queue-Soporte-555123456-20251025-150145.wav
Esto organiza las grabaciones por año/mes/día para fácil búsqueda.
💡 Sintaxis Completa de Queue() con Handlers
Queue(<nombre_cola>,<opciones>,<URL>,<anuncio_agente>,<timeout>,<AGI>,<gosub>,<rule>,<position>)
Parámetros usados en este taller:
opciones : tTb(queue-pre-bridge^s^1)
tT: habilita transferencia bidireccional con #
b(context^exten^priority): ejecuta el handler antes de puentear al agente
gosub : no se usa aquí (el handler viaja dentro de opciones)
Notas:
Para handlers adicionales (B, F, etc.) sigue la documentación oficial.
Ajusta el flag b() si necesitas ejecutar diferentes handlers por cola.
Ventajas de usar handler b:
✅ Grabación inicia solo cuando agente contesta
✅ No graba música en espera (ahorra espacio)
✅ Facilita auditoría (solo llamadas atendidas)
✅ Variables heredadas con __VARIABLE disponibles en handler
⚠️ Diferencia Crítica: Goto vs Gosub
Método antiguo (NO usar):
same => n,Goto(cola-handler,ventas,1) ; ❌ Pierde contexto
Método profesional (USAR):
same => n,Gosub(func-setup-queue-call,s,1) ; ✅ Retorna al contexto
same => n,Gosub(func-handle-queue-exit,s,1) ; ✅ Manejo limpio
Beneficios de Gosub:
Retorna al punto de llamada
Permite funciones reutilizables
Código más limpio y mantenible
Consistente con talleres previos (Taller 5)
Ahora que tenemos el handler profesional, debemos actualizar el IVR para usarlo.
Editar extensions_entradas.conf (del IVR de la Parte 1):
; === OPCIÓN 1: VENTAS ===
exten => 1,1,NoOp(=== Opción 1: Ventas ===)
same => n,Playback(custom/audio1)
same => n,Gosub(func-apply-open-hours,s,1)
same => n,GotoIf($[ "${HORARIO_STATUS}" = "OPEN" ]?open:closed)
same => n(open),Goto(cola-handler,ventas,1) ; Usar handler profesional
same => n(closed),Playback(custom/audio3)
; === OPCIÓN 2: SOPORTE ===
exten => 2,1,NoOp(=== Opción 2: Soporte ===)
same => n,Playback(custom/audio2)
same => n,Gosub(func-apply-open-hours,s,1)
same => n,GotoIf($[ "${HORARIO_STATUS}" = "OPEN" ]?open:closed)
same => n(open),Goto(cola-handler,soporte,1) ; Usar handler profesional
same => n(closed),Playback(custom/audio3)
💡 ¿Por qué Goto() aquí está bien?
Aunque generalmente preferimos Gosub(), usar Goto() para ir al handler de colas es correcto porque:
El handler maneja completamente la llamada hasta colgar
No necesitamos retornar al IVR después
Es más simple y directo
El handler usa Gosub() internamente para sus funciones
Si prefieres mantener el Queue() en el IVR pero con grabación:
; === OPCIÓN 1: VENTAS ===
exten => 1,1,NoOp(=== Opción 1: Ventas ===)
same => n,Playback(custom/audio1)
same => n,Gosub(func-apply-open-hours,s,1)
same => n,GotoIf($[ "${HORARIO_STATUS}" = "OPEN" ]?open:closed)
same => n(open),Set( __QUEUE_NAME =cola-ventas)
same => n,Set( __QUEUE_DEPT =Ventas)
same => n,Queue(${QUEUE_NAME},tTb(queue-pre-bridge^s^1)) ; Con handler b()
same => n,Playback(vm-nobodyavail)
same => n(closed),Playback(custom/audio3)
Los agentes necesitan poder loguearse y desloguearse de las colas (editar archivo extensions_anexos.conf).
Agregar en el contexto [anexos-internos]:
; ============================================================================
; CÓDIGOS DE FUNCIONES PARA AGENTES
; ============================================================================
; === LOGIN A COLA DE VENTAS (*71) ===
exten => *71,1,NoOp(=== Login a Cola de Ventas ===)
same => n,Set( AGENT_INTERFACE =PJSIP/${CHANNEL(endpoint)})
same => n,AddQueueMember(cola-ventas,${AGENT_INTERFACE})
same => n,NoOp(Resultado: ${AQMSTATUS})
same => n,GotoIf($[ "${AQMSTATUS}" = "ADDED" ]?success:error)
same => n(success),Playback(agent-loginok)
same => n(error),Playback(an-error-has-occurred)
; === LOGOUT DE COLA DE VENTAS (*72) ===
exten => *72,1,NoOp(=== Logout de Cola de Ventas ===)
same => n,Set( AGENT_INTERFACE =PJSIP/${CHANNEL(endpoint)})
same => n,RemoveQueueMember(cola-ventas,${AGENT_INTERFACE})
same => n,NoOp(Resultado: ${RQMSTATUS})
same => n,GotoIf($[ "${RQMSTATUS}" = "REMOVED" ]?success:error)
same => n(success),Playback(agent-loggedoff)
same => n(error),Playback(an-error-has-occurred)
; === LOGIN A COLA DE SOPORTE (*73) ===
exten => *73,1,NoOp(=== Login a Cola de Soporte ===)
same => n,Set( AGENT_INTERFACE =PJSIP/${CHANNEL(endpoint)})
same => n,AddQueueMember(cola-soporte,${AGENT_INTERFACE})
same => n,NoOp(Resultado: ${AQMSTATUS})
same => n,GotoIf($[ "${AQMSTATUS}" = "ADDED" ]?success:error)
same => n(success),Playback(agent-loginok)
same => n(error),Playback(an-error-has-occurred)
; === LOGOUT DE COLA DE SOPORTE (*74) ===
exten => *74,1,NoOp(=== Logout de Cola de Soporte ===)
same => n,Set( AGENT_INTERFACE =PJSIP/${CHANNEL(endpoint)})
same => n,RemoveQueueMember(cola-soporte,${AGENT_INTERFACE})
same => n,NoOp(Resultado: ${RQMSTATUS})
same => n,GotoIf($[ "${RQMSTATUS}" = "REMOVED" ]?success:error)
same => n(success),Playback(agent-loggedoff)
same => n(error),Playback(an-error-has-occurred)
; === PAUSAR EN TODAS LAS COLAS (*75) ===
exten => *75,1,NoOp(=== Pausar Agente en Todas las Colas ===)
same => n,Set( AGENT_INTERFACE =PJSIP/${CHANNEL(endpoint)})
same => n,PauseQueueMember(,${AGENT_INTERFACE})
same => n,Playback(custom/agent-pause) ; Graba este audio con *6500
; === DESPAUSAR EN TODAS LAS COLAS (*76) ===
exten => *76,1,NoOp(=== Despausar Agente en Todas las Colas ===)
same => n,Set( AGENT_INTERFACE =PJSIP/${CHANNEL(endpoint)})
same => n,UnpauseQueueMember(,${AGENT_INTERFACE})
same => n,Playback(custom/agent-unpause) ; Graba este audio con *6500
; === CONSULTAR ESTADO DE COLAS (*77) ===
exten => *77,1,NoOp(=== Consultar Estado de Colas ===)
same => n,Playback(queue-thereare)
same => n,SayNumber(${QUEUE_WAITING_COUNT(cola-ventas)})
same => n,Playback(queue-callswaiting)
same => n,Playback(in-the-queue)
same => n,Playback(digits/1) ; "Ventas"
same => n,Playback(queue-thereare)
same => n,SayNumber(${QUEUE_WAITING_COUNT(cola-soporte)})
same => n,Playback(queue-callswaiting)
same => n,Playback(in-the-queue)
same => n,Playback(digits/2) ; "Soporte"
💡 Resumen de Códigos
Código Función *71Login a Cola de Ventas *72Logout de Cola de Ventas *73Login a Cola de Soporte *74Logout de Cola de Soporte *75Pausar en todas las colas *76Despausar en todas las colas *77Consultar estado de colas 77777Simular llamada entrante
asterisk -rx "dialplan reload"
Verificar que no haya errores:
asterisk -rx "dialplan show from-pstn"
asterisk -rx "dialplan show ivr-principal"
asterisk -rx "dialplan show cola-handler"
asterisk -rx "dialplan show internal" | grep -A 5 "*7"
asterisk -rx "queue show"
Salida esperada:
cola-ventas has 0 calls (max 25) in 'rrmemory' strategy (0s holdtime, 0s talktime), W:0, C:0, A:0, SL:0.0% within 60s
cola-soporte has 0 calls (max 15) in 'fewestcalls' strategy (0s holdtime, 0s talktime), W:0, C:0, A:0, SL:0.0% within 60s
asterisk -rx "moh show classes"
Desde extensión 1001:
Marca *71 → Login a cola-ventas
Escucha “Agent login OK”
Desde extensión 1004:
Marca *73 → Login a cola-soporte
Escucha “Agent login OK”
Verificar en CLI:
asterisk -rx "queue show cola-ventas"
Salida esperada:
cola-ventas has 0 calls (max 25) in 'rrmemory' strategy (0s holdtime, 0s talktime), W:0, C:0, A:0, SL:0.0% within 60s
PJSIP/1001 (dynamic) (Not in use) has taken no calls yet
Desde extensión 1002:
Marca 77777 → Simula llamada entrante
Escucha mensaje de bienvenida
Marca 1 → Opción Ventas
Entra a cola-ventas
Escucha música en espera
El agente 1001 recibe la llamada
Contesta y conversa
Cuelga
Verificar grabación:
# Buscar archivo de grabación
find /var/spool/asterisk/monitor -name "*.wav" -type f -mmin -5
# Ver detalles (ajusta "Ventas" por el departamento correspondiente)
ls -lh /var/spool/asterisk/monitor/Ventas/ $( date +%Y/%m/%d ) /
Verificar CDR:
asterisk -rx "cdr show active"
Loguear 3 agentes en cola-ventas:
Desde 1001: *71
Desde 1002: *71
Desde 1003: *71
Generar 3 llamadas entrantes:
Desde 1004: 77777 → Marca 1 (Ventas)
Desde 1005: 77777 → Marca 1 (Ventas)
Desde softphone externo: 77777 → Marca 1 (Ventas)
Observar distribución:
Llamada 1 → 1001
Llamada 2 → 1002
Llamada 3 → 1003
Llamada 4 → 1001 (vuelve al inicio - round robin)
Desde extensión 1001:
Marca *75 → Pausar
Escucha “Agent pause”
Generar llamada:
Desde 1004: 77777 → Marca 1
La llamada NO va a 1001 (está pausado)
Va a 1002 o 1003
Despausar:
Desde 1001: *76 → Despausar
Ahora sí recibe llamadas
Desde cualquier extensión:
Marca *77
Escucha: “Hay X llamadas esperando en cola 1”
Escucha: “Hay X llamadas esperando en cola 2”
Para probar el comportamiento fuera de horario, tienes 3 opciones :
# Cambiar temporalmente a hora fuera de horario (ej: 20:00)
# Marca 77777 desde una extensión
# Deberías escuchar mensaje de fuera de horario
# Restaurar hora correcta
Editar la función de horarios:
nano /etc/asterisk/extensions_functions.conf
Cambiar:
exten => s,1,NoOp(=== Validando Horario de Atención ===)
same => n,GotoIfTime(09:00-18:00,mon-fri,*,*?open)
same => n,GotoIfTime(09:00-13:00,sat,*,*?open)
Por (forzar siempre cerrado):
exten => s,1,NoOp(=== Validando Horario de Atención - MODO PRUEBA ===)
same => n,GotoIfTime(00:00-00:01,sun,*,*?open) ; Imposible: solo 1 min el domingo
Recargar y probar:
asterisk -rx "dialplan reload"
# Escucha mensaje de fuera de horario
Restaurar:
# Volver a poner los horarios originales
nano /etc/asterisk/extensions_functions.conf
asterisk -rx "dialplan reload"
Agregar en contexto [internal]:
; === CÓDIGO DE PRUEBA: SIMULAR FUERA DE HORARIO ===
exten => 9001,1,NoOp(=== Prueba Fuera de Horario ===)
same => n,Set( HORARIO_STATUS =CLOSED) ; Forzar cerrado
same => n,Goto(from-pstn,123456789,1)
Probar:
asterisk -rx "dialplan reload"
# Marca 9001 (en lugar de 77777)
# Deberías escuchar mensaje de fuera de horario
💡 Recomendación
Para ambiente de pruebas: Usa la Opción A (cambiar hora del sistema)
Para ambiente de producción: Usa la Opción B pero con cuidado de restaurar
La Opción C es útil para pruebas rápidas sin afectar el horario real.
Si completaste el Taller 8, tus grabaciones ya están integradas con CDR/CEL en PostgreSQL.
Las grabaciones se guardan automáticamente en el campo userfield del CDR:
- u postgres psql aulaswitch
-- Ver últimas llamadas con grabación
userfield as recording_path
WHERE userfield LIKE '%queue-%'
Salida esperada:
calldate | src | dst | duration | disposition | recording_path
---------------------------+----------+------+----------+-------------+------------------------------------------
2025-01-19 14:30:22-05 | 1002 | 1001 | 125 | ANSWERED | /var/spool/asterisk/monitor/2025/01/19/queue-Ventas-1002-20250119-143022.wav
2025-01-19 14:25:15-05 | 1004 | 1005 | 89 | ANSWERED | /var/spool/asterisk/monitor/2025/01/19/queue-Soporte-1004-20250119-142515.wav
Llamadas de cola por día:
COUNT ( * ) as total_llamadas,
AVG (duration) as duracion_promedio,
COUNT ( CASE WHEN disposition = 'ANSWERED' THEN 1 END ) as atendidas,
COUNT ( CASE WHEN disposition = 'NO ANSWER' THEN 1 END ) as no_atendidas
WHERE userfield LIKE '%queue-%'
AND DATE (calldate) = CURRENT_DATE
Top agentes por llamadas atendidas:
COUNT ( * ) as llamadas_atendidas,
AVG (duration) as duracion_promedio,
MAX (duration) as llamada_mas_larga
WHERE userfield LIKE '%queue-%'
AND disposition = 'ANSWERED'
AND DATE (calldate) = CURRENT_DATE
ORDER BY llamadas_atendidas DESC ;
Llamadas con grabación disponible:
duration as duracion_seg,
SUBSTRING (userfield FROM 'queue-([^-]+)' ) as departamento,
userfield as archivo_audio
WHERE userfield LIKE '%queue-%'
AND disposition = 'ANSWERED'
-- Crear vista para análisis de colas
CREATE OR REPLACE VIEW v_queue_performance AS
SUBSTRING (userfield FROM 'queue-([^-]+)' ) as departamento,
WHEN disposition = 'ANSWERED' THEN 'Atendida'
WHEN disposition = 'NO ANSWER' THEN 'No contestada'
WHEN disposition = 'BUSY' THEN 'Ocupado'
userfield as recording_path
WHERE userfield LIKE '%queue-%' ;
SELECT * FROM v_queue_performance
WHERE fecha = CURRENT_DATE
asterisk -rx "queue show"
asterisk -rx "queue show cola-ventas"
asterisk -rx "queue show cola-ventas" | grep -A 10 "Members:"
asterisk -rx "queue show cola-ventas" | grep -A 10 "Callers:"
asterisk -rx "core show channels"
asterisk -rx "core show channels" | grep Queue
# Ver archivos de grabación del día
ls -lht /var/spool/asterisk/monitor/ $( date +%Y/%m/%d ) / | head -10
# Ver tamaño total de grabaciones
du -sh /var/spool/asterisk/monitor/ $( date +%Y/%m/%d ) /
# Reproducir última grabación (requiere sox)
play $( find /var/spool/asterisk/monitor -name "*.wav" -type f -mmin -5 | head -1 )
tail -f /var/log/asterisk/queue_log
tail -f /var/log/asterisk/full | grep -i queue
asterisk -rx "queue show cola-ventas"
Interpretar salida:
cola-ventas has 2 calls (max 25) in 'rrmemory' strategy (35s holdtime, 180s talktime), W:0, C:15, A:2, SL:86.7% within 60s
2 calls: 2 llamadas actualmente en cola
35s holdtime: Tiempo promedio de espera
180s talktime: Duración promedio de llamadas
W:0: 0 llamadas esperando (Weight)
C:15: 15 llamadas completadas
A:2: 2 llamadas abandonadas
SL:86.7%: 86.7% atendidas en menos de 60s
El archivo /var/log/asterisk/queue_log contiene eventos detallados:
tail -20 /var/log/asterisk/queue_log
Formato:
timestamp|uniqueid|queue|agent|event|data1|data2|data3|data4|data5
Eventos principales:
ENTERQUEUE: Llamada entra a cola
CONNECT: Agente contesta
COMPLETEAGENT: Agente cuelga
COMPLETECALLER: Llamante cuelga
ABANDON: Llamante cuelga antes de ser atendido
ADDMEMBER: Agente se loguea
REMOVEMEMBER: Agente se desloguea
PAUSE: Agente se pausa
UNPAUSE: Agente se despausa
Si tienes PostgreSQL configurado (Taller 8):
-- Llamadas de hoy por cola
COUNT ( * ) as total_llamadas,
AVG (duration) as duracion_promedio,
SUM (duration) as duracion_total
WHERE DATE (calldate) = CURRENT_DATE
AND userfield LIKE '%queue-%'
-- Top 5 agentes por llamadas atendidas
COUNT ( * ) as llamadas_atendidas,
AVG (duration) as duracion_promedio
WHERE DATE (calldate) = CURRENT_DATE
AND disposition = 'ANSWERED'
AND dst IN ( '1001' , '1002' , '1003' , '1004' , '1005' )
ORDER BY llamadas_atendidas DESC
Verificar:
asterisk -rx "queue show cola-ventas" | grep PJSIP/1001
# Buscar "(paused)" en la salida
# ¿Está registrado el dispositivo?
asterisk -rx "pjsip show endpoints" | grep 1001
Solución:
asterisk -rx "queue unpause member PJSIP/1001 queue cola-ventas"
Verificar:
asterisk -rx "moh show classes"
ls -lh /var/lib/asterisk/moh/
Solución:
asterisk -rx "moh reload"
chown -R asterisk:asterisk /var/lib/asterisk/moh/
Verificar:
ls -ld /var/spool/asterisk/monitor/
ls -l /var/spool/asterisk/monitor/
Solución:
# Crear directorio y dar permisos
mkdir -p /var/spool/asterisk/monitor
chown -R asterisk:asterisk /var/spool/asterisk/monitor
chmod 755 /var/spool/asterisk/monitor
Verificar dialplan:
asterisk -rx "dialplan show from-pstn"
asterisk -rx "dialplan show ivr-principal"
Ver logs:
tail -f /var/log/asterisk/full
Crear script de limpieza:
nano /usr/local/bin/cleanup-recordings.sh
Contenido:
# Eliminar grabaciones mayores a 90 días
find /var/spool/asterisk/monitor -name "*.wav" -type f -mtime +90 -delete
find /var/spool/asterisk/monitor -type d -empty -delete
echo "$( date ): Limpieza completada" >> /var/log/asterisk/cleanup.log
Dar permisos y programar:
chmod +x /usr/local/bin/cleanup-recordings.sh
Agregar:
0 2 * * * /usr/local/bin/cleanup-recordings.sh
nano /usr/local/bin/backup-queue-log.sh
Contenido:
TIMESTAMP = $( date +%Y%m%d )
cp /var/log/asterisk/queue_log /root/backups/queue_log. $TIMESTAMP
gzip /root/backups/queue_log. $TIMESTAMP
Crear script de monitoreo:
nano /usr/local/bin/monitor-queues.sh
Contenido:
for QUEUE in cola-ventas cola-soporte ; do
WAITING = $( asterisk -rx "queue show $QUEUE " | grep "has" | awk '{print $3}' )
if [ " $WAITING " -gt " $THRESHOLD " ]; then
echo "ALERTA: Cola $QUEUE tiene $WAITING llamadas esperando" | mail -s "Alerta de Cola" admin@techsolutions.com
✅ Central telefónica completa con IVR, horarios y colas
✅ Handlers profesionales b/B para grabación automática (Asterisk 22)
✅ IVR con Background() para respuesta inmediata
✅ Sistema de grabación de audios (*6500) para personalización
✅ 2 colas profesionales con estrategias optimizadas
✅ Uso de Gosub() en lugar de Goto() (consistente con Taller 5)
✅ Sistema de login/logout para agentes (*71-*77)
✅ Grabación solo de llamadas atendidas (ahorra espacio)
✅ Integración completa con PostgreSQL (Taller 8)
✅ Variables heredadas (__VARIABLE) para handlers
✅ Contexto internal consistente con talleres previos
✅ /etc/asterisk/pjsip_wizard.conf → 5 extensiones con plantilla
✅ /etc/asterisk/queues.conf → Colas con configuración profesional
✅ /etc/asterisk/musiconhold.conf → MOH por departamento
✅ /etc/asterisk/extensions_functions.conf → Horarios + Grabación audios
✅ /etc/asterisk/extensions_entradas.conf → IVR + Handlers + Funciones
✅ /etc/asterisk/extensions.conf → Configuración principal
🎯 Handlers b en Queue() → Grabación solo cuando agente contesta
🎯 Background() en IVR → Entrada DTMF durante reproducción
🎯 Gosub() en lugar de Goto() → Funciones reutilizables y retorno limpio
🎯 Variables heredadas __VAR → Disponibles en todos los contextos
🎯 Integración PostgreSQL → Reportes avanzados con SQL
🎯 Sistema de audios interno → Sin dependencias externas
🎯 Organización modular → Archivos separados por función
Aspecto ❌ Método Antiguo ✅ Método Profesional Grabación MixMonitor manual Handler b automático Contexto call-centerinternal (consistente)IVR Playback() bloqueanteBackground() interactivoFlujo Goto() sin retornoGosub() con Return()Variables Locales se pierden __VAR heredadasAudios Software externo Sistema interno (*6500)
🎓 Taller 11: Dashboard de Colas en Tiempo Real
Agregaremos al panel web del Taller 9 un dashboard de monitoreo que muestre:
Estado de colas (llamadas en espera, tiempo promedio)
Estado de agentes (disponible, ocupado, pausado, tiempo en llamada)
Llamadas activas (quién está atendiendo a quién)
Métricas en vivo (llamadas atendidas, abandonadas, SL)
Control remoto (pausar/despausar agentes desde la web)
Usaremos AMI (Asterisk Manager Interface) para eventos en tiempo real y Server-Sent Events (SSE) para actualización del dashboard.
Has construido una central telefónica profesional usando las mejores prácticas de Asterisk 22:
✨ Handlers modernos para control granular
✨ Arquitectura modular fácil de mantener
✨ Integración completa con base de datos
✨ Consistencia técnica con talleres previos
✨ Lista para producción con todas las funcionalidades
FIN DEL TALLER 10 - CENTRAL TELEFÓNICA COMPLETA 🚀