PJSIPNotify en Asterisk 20: Control Programático de Mensajes SIP NOTIFY desde el Dialplan

Asterisk 20.10.0 introdujo una característica largamente esperada por la comunidad: la aplicación de dialplan PJSIPNotify. Esta nueva funcionalidad permite enviar mensajes SIP NOTIFY directamente desde el dialplan, eliminando la necesidad de depender exclusivamente del CLI o AMI para esta tarea.

En este artículo exploraremos en profundidad esta nueva capacidad, sus casos de uso prácticos, y cómo implementarla en tus sistemas de producción.

¿Qué es SIP NOTIFY?

Fundamentos del Protocolo

SIP NOTIFY es un método definido en RFC 3265 que permite a un servidor o endpoint SIP informar a suscriptores sobre cambios de estado o eventos específicos. Es el complemento del método SUBSCRIBE: un dispositivo se suscribe a un evento, y el servidor envía NOTIFY cuando ese evento ocurre.

Flujo básico SUBSCRIBE/NOTIFY:

Teléfono SIP              Servidor PBX
    |                          |
    |----SUBSCRIBE MWI-------->|
    |<-------200 OK------------|
    |<------NOTIFY (0 msgs)----|
    |-------200 OK------------>|
    |                          |
    [Llega un mensaje de voz]
    |                          |
    |<------NOTIFY (1 msg)-----|
    |-------200 OK------------>|
    |  [Luz MWI se enciende]   |

Casos de Uso Comunes

Los mensajes NOTIFY se utilizan ampliamente para:

  1. Message Waiting Indicator (MWI): Indicador luminoso de mensajes de voz
  2. Busy Lamp Field (BLF): Estado de otras extensiones (ocupado/libre/sonando)
  3. Presence: Estado de disponibilidad de usuarios
  4. Call Pickup: Notificación de llamadas disponibles para captura
  5. Auto-provisioning: Solicitud de reconfiguration del dispositivo
  6. Custom Events: Eventos personalizados específicos de aplicación

La Limitación Histórica

Antes de Asterisk 20.10.0, enviar mensajes NOTIFY manualmente requería:

Opción 1: Desde CLI

asterisk -rx "pjsip send notify check-voicemail endpoint 1001"

Opción 2: Desde AMI (Asterisk Manager Interface)

Action: PJSIPNotify
Endpoint: 1001
Variable: Event=check-voicemail

Problemas con estos métodos:

  • No se pueden enviar desde el dialplan durante una llamada activa
  • Requieren acceso CLI o AMI (problemas de seguridad)
  • Difícil integración en lógica de llamadas
  • No se pueden enviar dentro del diálogo SIP actual

PJSIPNotify: La Nueva Aplicación

Sintaxis

PJSIPNotify([to],content)

Parámetros:

  • to (opcional): URI SIP arbitraria a la que enviar el NOTIFY

    • Si se omite, envía dentro del diálogo SIP actual del canal
    • Formato: <sip:usuario@dominio:puerto>
  • content: Contenido del mensaje

    • Puede ser una entrada pre-configurada en pjsip_notify.conf
    • O una lista de headers especificados en dialplan con formato &Header=Valor

Requisito Importante

Para enviar NOTIFY a URIs arbitrarias (fuera del diálogo actual), debes configurar default_outbound_endpoint en la sección [global] de pjsip.conf:

[global]
default_outbound_endpoint=default-outbound

[default-outbound]
type=endpoint
context=from-internal
disallow=all
allow=ulaw
allow=alaw

Este endpoint determina el contacto del mensaje NOTIFY saliente.

Ejemplos Prácticos

Ejemplo 1: NOTIFY Dentro del Diálogo Actual

Escenario: Enviar un evento personalizado durante una llamada activa.

[test-notify]
exten => 7000,1,Answer()
 same => n,Playback(hello-world)
 same => n,PJSIPNotify(,&Event=Test&X-Data=Fun)
 same => n,Wait(2)
 same => n,Hangup()

Resultado SIP:

NOTIFY sip:1001@192.168.1.10:5060 SIP/2.0
Event: Test
X-Data: Fun
Content-Length: 0

Ejemplo 2: NOTIFY con Configuración Pre-definida

Paso 1: Configurar el template en pjsip_notify.conf

[potato-notify]
Event=>PotatoReport
Content-type=>application/potato-summary
Content=>Num-Potatoes: 23
Content=>Potato-Quality: fair
Content=>

Paso 2: Usar desde dialplan

[potato-report]
exten => 7001,1,Answer()
 same => n,PJSIPNotify(,potato-notify)
 same => n,Hangup()

Resultado SIP:

NOTIFY sip:1001@192.168.1.10:5060 SIP/2.0
Event: PotatoReport
Content-Type: application/potato-summary
Content-Length: 38

Potato-Quality: fair
Num-Potatoes: 23

Ejemplo 3: NOTIFY a URI Arbitraria

Escenario: Enviar notificación a un endpoint específico desde el dialplan.

[notify-endpoint]
exten => 7002,1,Answer()
 same => n,Set(TARGET_URI=<sip:bob@127.0.0.1:5260>)
 same => n,PJSIPNotify(${TARGET_URI},&Event=Custom&X-Message=Hello Bob)
 same => n,Playback(notification-sent)
 same => n,Hangup()

Ejemplo 4: Message Waiting Indicator (MWI)

Configuración en pjsip_notify.conf:

[mwi-update]
Event=>message-summary
Content-type=>application/simple-message-summary
Content=>Messages-Waiting: yes
Content=>Voice-Message: 2/0 (0/0)
Content=>

Dialplan:

[update-mwi]
exten => 8000,1,Answer()
 same => n,Verbose(1,Updating MWI for extension ${EXTEN})
 same => n,PJSIPNotify(,mwi-update)
 same => n,Playback(mwi-updated)
 same => n,Hangup()

Ejemplo 5: Forced Answer (Auto-Answer)

Algunos teléfonos SIP soportan auto-answer mediante NOTIFY con headers especiales.

pjsip_notify.conf:

[force-answer]
Event=>talk
Content-type=>application/x-broadworks-callinfo
Content=>call-info: ;answer-after=0
Content=>

Dialplan - Intercomunicador:

[intercom]
exten => _*80X.,1,NoOp(Intercom to ${EXTEN:3})
 same => n,Set(TARGET_EXTEN=${EXTEN:3})
 same => n,Set(TARGET_URI=<sip:${TARGET_EXTEN}@${SIPDOMAIN}>)
 same => n,PJSIPNotify(${TARGET_URI},force-answer)
 same => n,Wait(1)
 same => n,Dial(PJSIP/${TARGET_EXTEN},30,A(beep))
 same => n,Hangup()

Uso: Marcar *80 + extensión para activar auto-answer en el teléfono destino.

Casos de Uso Avanzados

Caso 1: Sistema de Paging con Auto-Answer

Objetivo: Crear un sistema de paging donde múltiples teléfonos responden automáticamente.

pjsip_notify.conf:

[paging-alert]
Event=>talk
Content-type=>application/x-broadworks-callinfo
Content=>call-info: ;answer-after=0;priority=emergency
Content=>

extensions.conf:

[paging-system]
; Paging a grupo de extensiones
exten => *70,1,Answer()
 same => n,Set(PAGING_GROUP=1001&1002&1003&1004)
 same => n,Playback(paging-announcement)
 same => n,Set(i=1)
 same => n,While($[${i} <= ${FIELDQTY(PAGING_GROUP,&)}])
 same => n,Set(EXTEN=${CUT(PAGING_GROUP,&,${i})})
 same => n,Set(URI=<sip:${EXTEN}@${SIPDOMAIN}>)
 same => n,PJSIPNotify(${URI},paging-alert)
 same => n,Set(i=$[${i} + 1])
 same => n,EndWhile
 same => n,Wait(2)
 same => n,Page(${PAGING_GROUP},,A(beep))
 same => n,Hangup()

Caso 2: Actualización Dinámica de BLF

Objetivo: Actualizar el estado BLF de una extensión cuando cambia su estado de disponibilidad.

pjsip_notify.conf:

[blf-busy]
Event=>dialog
Content-type=>application/dialog-info+xml
Content=><?xml version="1.0"?>
Content=><dialog-info xmlns="urn:ietf:params:xml:ns:dialog-info"
Content=>  version="1" state="full" entity="sip:${EXTEN}@${SIPDOMAIN}">
Content=>  <dialog id="1">
Content=>    <state>confirmed</state>
Content=>  </dialog>
Content=></dialog-info>
Content=>

[blf-free]
Event=>dialog
Content-type=>application/dialog-info+xml
Content=><?xml version="1.0"?>
Content=><dialog-info xmlns="urn:ietf:params:xml:ns:dialog-info"
Content=>  version="1" state="full" entity="sip:${EXTEN}@${SIPDOMAIN}">
Content=>  <dialog id="1">
Content=>    <state>terminated</state>
Content=>  </dialog>
Content=></dialog-info>
Content=>

extensions.conf:

[blf-control]
; Marcar *71 para aparecer ocupado
exten => *71,1,Answer()
 same => n,Set(DEVICE_STATE(Custom:DND${CALLERID(num)})=INUSE)
 same => n,PJSIPNotify(,blf-busy)
 same => n,Playback(do-not-disturb&activated)
 same => n,Hangup()

; Marcar *70 para aparecer libre
exten => *70,1,Answer()
 same => n,Set(DEVICE_STATE(Custom:DND${CALLERID(num)})=NOT_INUSE)
 same => n,PJSIPNotify(,blf-free)
 same => n,Playback(do-not-disturb&de-activated)
 same => n,Hangup()

Caso 3: Rechazo Automático de Llamadas (Call Rejection)

Objetivo: Enviar NOTIFY para indicar al teléfono que rechace una llamada entrante automáticamente.

pjsip_notify.conf:

[reject-call]
Event=>call-completion
Content-type=>application/x-call-rejection
Content=>Reject-Reason: busy-everywhere
Content=>

extensions.conf con Lista Negra:

[from-trunk]
exten => _X.,1,NoOp(Incoming call from ${CALLERID(num)})
 same => n,GotoIf($[${BLACKLIST()}=1]?blacklisted:accept)
 same => n(blacklisted),Set(CALLER_URI=<sip:${CALLERID(num)}@${SIPDOMAIN}>)
 same => n,PJSIPNotify(${CALLER_URI},reject-call)
 same => n,Busy(20)
 same => n,Hangup()
 same => n(accept),Dial(PJSIP/${EXTEN})
 same => n,Hangup()

Caso 4: Provisioning Remoto de Teléfonos

Objetivo: Forzar reconfiguration de teléfonos desde el dialplan.

pjsip_notify.conf:

[polycom-check-cfg]
Event=>check-sync
Content-Length=>0

[yealink-autoprov]
Event=>check-sync;reboot=false
Content-Length=>0

[cisco-resync]
Event=>check-sync
Content-type=>application/url
Content=>http://provisioning.example.com/cfg/${MAC}.xml
Content=>

extensions.conf:

[admin-tools]
; Reconfigurar teléfono Polycom
exten => *90XXX,1,Answer()
 same => n,Set(EXTEN_NUM=${EXTEN:3})
 same => n,Set(TARGET_URI=<sip:${EXTEN_NUM}@${SIPDOMAIN}>)
 same => n,PJSIPNotify(${TARGET_URI},polycom-check-cfg)
 same => n,Playback(configuration-updated)
 same => n,Hangup()

; Reconfigurar todos los teléfonos de un tipo
exten => *91,1,Answer()
 same => n,Playback(please-wait)
 same => n,System(/usr/local/bin/bulk-reprov.sh polycom)
 same => n,Playback(completed)
 same => n,Hangup()

Script bulk-reprov.sh:

#!/bin/bash
PHONE_TYPE=$1

case $PHONE_TYPE in
    polycom)
        NOTIFY_CFG="polycom-check-cfg"
        ;;
    yealink)
        NOTIFY_CFG="yealink-autoprov"
        ;;
    cisco)
        NOTIFY_CFG="cisco-resync"
        ;;
esac

# Obtener lista de endpoints del tipo especificado
asterisk -rx "pjsip show endpoints" | grep "$PHONE_TYPE" | while read line; do
    ENDPOINT=$(echo $line | awk '{print $1}')
    asterisk -rx "pjsip send notify $NOTIFY_CFG endpoint $ENDPOINT"
    sleep 1
done

Mejora del CLI: pjsip send notify channel

Junto con PJSIPNotify, se mejoró el comando CLI para soportar envío a canales específicos:

Nueva sintaxis:

pjsip send notify <option> channel <channel>

Ejemplo:

# Enviar a endpoint (anterior)
pjsip send notify check-voicemail endpoint 1001

# NUEVO: Enviar a canal específico
pjsip send notify check-voicemail channel PJSIP/1001-00000001

Esto permite enviar NOTIFY dentro del contexto de una llamada activa desde CLI.

Debugging y Troubleshooting

Habilitar Logging SIP

Para ver los mensajes NOTIFY en los logs:

# CLI
asterisk -rvvvv
pjsip set logger on

O en logger.conf:

[logfiles]
full => notice,warning,error,debug,verbose
messages => notice,warning,error

Captura de Paquetes

Usar sngrep para visualizar el flujo SIP:

sngrep port 5060

Filtra por NOTIFY:

F7 > Method: NOTIFY

Verificar Configuración

# Ver configuración de notify
asterisk -rx "pjsip show notify"

# Verificar endpoint default
asterisk -rx "pjsip show endpoint default-outbound"

# Test de envío
asterisk -rx "pjsip send notify test-notify endpoint 1001"

Problemas Comunes

Problema 1: "No default outbound endpoint configured"

WARNING: No default outbound endpoint configured

Solución: Configurar en pjsip.conf:

[global]
default_outbound_endpoint=default-outbound

Problema 2: Dispositivo no responde a NOTIFY

Verificar que el dispositivo soporte el Event específico:

# Capturar SUBSCRIBE del dispositivo para ver qué eventos solicita
sngrep
# Filtrar por SUBSCRIBE y revisar header Event:

Problema 3: NOTIFY enviado pero sin efecto

Algunos dispositivos requieren headers específicos:

[custom-notify]
Event=>talk
Content-type=>application/x-broadworks-callinfo
Content=>call-info: ;answer-after=0
; Header específico del fabricante:
X-Vendor-Specific=>custom-value
Content=>

Integración con AMI

Aunque PJSIPNotify es para dialplan, puedes combinarlo con AMI para control externo:

Ejemplo en Python:

import asterisk.manager

def send_notify_via_dialplan(extension, notify_config):
    """Envía NOTIFY ejecutando contexto de dialplan vía AMI"""
    
    manager = asterisk.manager.Manager()
    manager.connect('localhost')
    manager.login('admin', 'secret')
    
    # Originar llamada que ejecuta PJSIPNotify
    response = manager.originate(
        channel=f'Local/{extension}@send-notify',
        context='send-notify-handler',
        exten='s',
        priority=1,
        variables={
            'NOTIFY_CONFIG': notify_config,
            'TARGET_EXTEN': extension
        }
    )
    
    manager.logoff()
    return response

Dialplan correspondiente:

[send-notify]
exten => _X.,1,NoOp(Trigger notify for ${EXTEN})
 same => n,Return()

[send-notify-handler]
exten => s,1,NoOp(Sending ${NOTIFY_CONFIG} to ${TARGET_EXTEN})
 same => n,Set(URI=<sip:${TARGET_EXTEN}@${SIPDOMAIN}>)
 same => n,PJSIPNotify(${URI},${NOTIFY_CONFIG})
 same => n,Hangup()

Comparación: Antes vs Después

Antes de PJSIPNotify

Escenario: Enviar MWI update después de procesar voicemail.

[voicemail-handler]
exten => s,1,VoiceMail(${EXTEN}@default,u)
 same => n,System(echo "pjsip send notify mwi-update endpoint ${EXTEN}" | asterisk -rx)
 same => n,Hangup()

Problemas:

  • Llamada externa a shell
  • Performance overhead
  • Difícil de debuggear
  • Posibles race conditions

Después de PJSIPNotify

[voicemail-handler]
exten => s,1,VoiceMail(${EXTEN}@default,u)
 same => n,PJSIPNotify(,mwi-update)
 same => n,Hangup()

Ventajas:

  • Nativo del dialplan
  • Mejor performance
  • Dentro del diálogo SIP actual
  • Logging integrado

Best Practices

1. Usar Configuraciones Pre-definidas

En lugar de:

same => n,PJSIPNotify(,&Event=check-sync&Content-type=text/plain&Content=update)

Mejor:

; pjsip_notify.conf
[device-update]
Event=>check-sync
Content-type=>text/plain
Content=>update
Content=>

; extensions.conf
same => n,PJSIPNotify(,device-update)

Razón: Más legible, reutilizable, fácil de mantener.

2. Validar URIs Dinámicas

[safe-notify]
exten => 8000,1,NoOp(Notify to ${TARGET})
 same => n,GotoIf($[${LEN(${TARGET})} = 0]?error)
 same => n,GotoIf($["${TARGET}" = ""]?error)
 same => n,Set(URI=<sip:${TARGET}@${SIPDOMAIN}>)
 same => n,PJSIPNotify(${URI},custom-notify)
 same => n,Hangup()
 same => n(error),Playback(invalid)
 same => n,Hangup()

3. Logging Apropiado

same => n,Verbose(1,Sending ${NOTIFY_TYPE} NOTIFY to ${TARGET_URI})
same => n,PJSIPNotify(${TARGET_URI},${NOTIFY_TYPE})
same => n,NoOp(PJSIPNOTIFY result: ${PJSIPNOTIFYSTATUS})

4. Manejo de Errores

same => n,PJSIPNotify(${URI},check-sync)
same => n,GotoIf($["${PJSIPNOTIFYSTATUS}" = "SUCCESS"]?ok:failed)
same => n(failed),Log(ERROR,Failed to send NOTIFY to ${URI})
same => n,Goto(cleanup)
same => n(ok),Verbose(1,NOTIFY sent successfully)
same => n(cleanup),Hangup()

5. Rate Limiting

Si envías NOTIFY en bucle, implementa rate limiting:

[bulk-notify]
exten => 9000,1,Answer()
 same => n,Set(EXTENSIONS=1001&1002&1003&1004&1005)
 same => n,Set(i=1)
 same => n,While($[${i} <= ${FIELDQTY(EXTENSIONS,&)}])
 same => n,Set(EXT=${CUT(EXTENSIONS,&,${i})})
 same => n,Set(URI=<sip:${EXT}@${SIPDOMAIN}>)
 same => n,PJSIPNotify(${URI},update-config)
 same => n,Wait(0.5)  ; Rate limiting: 500ms entre mensajes
 same => n,Set(i=$[${i} + 1])
 same => n,EndWhile
 same => n,Hangup()

Consideraciones de Seguridad

1. Validación de Input

[user-notify]
; MAL - Vulnerable a injection
exten => 7000,1,Set(MSG=${CALLERID(name)})
 same => n,PJSIPNotify(,&Event=UserMessage&X-Data=${MSG})

; BIEN - Sanitizado
exten => 7000,1,Set(MSG=${FILTER(A-Za-z0-9 ,${CALLERID(name)})})
 same => n,GotoIf($[${LEN(${MSG})} > 50]?truncate)
 same => n,Goto(send)
 same => n(truncate),Set(MSG=${MSG:0:50})
 same => n(send),PJSIPNotify(,&Event=UserMessage&X-Data=${MSG})

2. Restricción de Destinos

[notify-restricted]
exten => _90XX,1,NoOp(Admin notify to ${EXTEN:2})
 same => n,GotoIf($["${CALLERID(num)}" != "1000"]?unauthorized)
 same => n,Set(TARGET=${EXTEN:2})
 same => n,PJSIPNotify(<sip:${TARGET}@${SIPDOMAIN}>,admin-msg)
 same => n,Hangup()
 same => n(unauthorized),Playback(access-denied)
 same => n,Hangup()

3. Audit Logging

[logged-notify]
exten => 8000,1,NoOp(=== NOTIFY Request ===)
 same => n,Set(CDR(userfield)=NOTIFY:${NOTIFY_TYPE}:${TARGET})
 same => n,Log(NOTICE,User ${CALLERID(num)} sending ${NOTIFY_TYPE} to ${TARGET})
 same => n,PJSIPNotify(${TARGET_URI},${NOTIFY_TYPE})
 same => n,Hangup()

Conclusión

PJSIPNotify en Asterisk 20+ representa una mejora significativa en el control programático de mensajes SIP NOTIFY. Las principales ventajas son:

Ventajas Técnicas:

  • Control nativo desde dialplan
  • Sin overhead de llamadas externas
  • Integración seamless en lógica de llamadas
  • Mejor performance y debugging

Casos de Uso Habilitados:

  • MWI dinámico durante llamadas
  • Auto-answer/paging systems
  • Provisioning remoto automático
  • Custom event notifications
  • Integración con aplicaciones externas

Mejores Prácticas:

  • Usar configuraciones pre-definidas en pjsip_notify.conf
  • Implementar validación y sanitización de inputs
  • Logging apropiado para auditoría
  • Rate limiting en operaciones bulk
  • Manejo de errores robusto

La combinación de PJSIPNotify con las capacidades existentes de Asterisk abre nuevas posibilidades para integración de telefonía y aplicaciones, especialmente en escenarios donde se requiere control fino del comportamiento de endpoints durante llamadas activas.

Referencias


¿Has implementado PJSIPNotify en tu sistema? Comparte tu experiencia en los comentarios.

 

Vota el Articulo: 

Sin votos (todavía)
Evalúa la calidad del articulo
Suscribirse a Comentarios de "PJSIPNotify en Asterisk 20: Control Programático de Mensajes SIP NOTIFY desde el Dialplan" Suscribirse a VozToVoice - Todos los comentarios