Monitoring Calls on 3cX Call Control API

Monitoring Calls on 3cX Call Control API

Straight to the point. You can monitor calls using two mechanisms.

1) PBX Events 2) Call Monitoring (best for me)

Using PBX Events. While creating your connection, you can easily subscribe to ALL events coming from 3cX. The events include agent registration, PBX updates, call updates and of course auto events.

In my previous post about Call Control API , I have demonstrated the Bootstrap method which resets your connection to 3cX server when your app initializes.

Here is another code extract.

        public static PhoneSystem Bootstrap(int wait)
        {
            try
            {
               //...other connection codes above

                var ps = PhoneSystem.Reset(
               PhoneSystem.ApplicationName + new Random(Environment.TickCount).Next().ToString(),
               "127.0.0.1",
               int.Parse(iniContent["ConfService"]["ConfPort"]),
               iniContent["ConfService"]["confUser"],
               iniContent["ConfService"]["confPass"]);

                // Event data can be found here inside ConfObject.
                ps.Inserted += (x, y) =>
                {
                    //Logger.Log(y?.ConfObject?.ToString());

                };
                ps.Updated += (x, y) =>
                {
                    if (y.ConfObject != null)
                    {
                        try
                        {
                          //The snapshot will give direct access to a call state if the ConfObject is a call data object
                            //var test = (OMCallCollector.CallStateSnapshot)y.ConfObject;
                            //Logger.Log(test.Destination);
                        }
                        catch (Exception ex)
                        {

                        }
                    }
                };
                ps.Deleted += (x, y) =>
                {

                    if (y.ConfObject != null)
                    {

                    }
                };
                ps.WaitForConnect(TimeSpan.FromSeconds(wait));
                //...other connection codes below
            }
            catch (Exception ex)
            {
            }
        }

Subscribing to this will give you accurate call data. You just have to check if the ConfObject actually has call data or just something else.

Using Call Monitoring

I prefer using Call Monitoring. It's straight to the point and gets live/active call data from the CallStorage. You can call this sample method and here is the implementation.

 internal static void Monitor()
        {
            try
            {
                var omcache = PhoneSystem.Root.CallStorage;
                {
                    omcache.Updated += (id, state) => Monitor(state);
                    omcache.Removed += (id, state) => Monitor(state);
                }
            }
            catch (Exception ex)
            {
                Logger.Log(ex);
            }
        }

The state parameter is of type OMCallCollector.CallStateSnapshot and it has an inbuilt .ToString() method which prints some basic properties as Key-Value pairs. Such a lovely object for C# Dictionary.

A problem I had with call monitoring was that it kept stopping and Call updates stopped coming after some idle processes. Sometimes, it has stopped even while frequent calls were being made. For solutions, I thought of initiating the root class as a Singleton using Dependency Injection for ASP.Net Core but I changed my mind. A better and super way was to create a service to ping an API method which checks for connection every minute. This has been reliable and has never failed.

OMCallCollector.CallStateSnapshot Data Model You can get many properties of a call directly from accessing the state properties.

Here is the Data Model

 public enum LocalConnectionState
        {
            UnknownState = 0,
            Ringing = 1,
            Dialing = 2,
            Connected = 3,
            WaitingForNewParty = 4,
            TryingToTransfer = 5
        }
        public enum ConnectionCapabilities
        {
            CC_None = 0,
            CC_Drop = 1,
            CC_Divert = 2,
            CC_Transfer = 4,
            CC_Pickup = 8,
            CC_BargeIn = 16
        }
        public enum DnType
        {
            None = 0,
            Extension = 1,
            Queue = 2,
            RingGroup = 4,
            IVR = 8,
            Fax = 16,
            Conference = 32,
            Parking = 64,
            ExternalLine = 128,
            SpecialMenu = 256
        }

        public class ActiveConnectionFields
        {
            public ActiveConnectionFields(ActiveConnection ac);

            public ActiveConnection ac { get; }
            public DN dn { get; }
            public int dnhash { get; }
            public int onbehalf { get; }

            public override string ToString();
        }
        public class CallStateSnapshot
        {
            public CallStateSnapshot(int id);

            public int ID { get; }
            public ActiveConnectionFields Transferror { get; }
            public List<ActiveConnectionFields> Bargein { get; }
            public List<ActiveConnectionFields> TalkTo { get; }
            public List<ActiveConnectionFields> RoutingTo { get; }
            public string Destination { get; }
            public ActiveConnectionFields Owner { get; }
            public CallState State { get; }
            public HashSet<int> Participants { get; }
            public DateTime? AnsweredAt { get; }
            public DateTime? StartedAt { get; }

            public static string GetCallerID(ActiveConnection ac, DN theDN);
            public static string GetCallerName(ActiveConnection ac, DN theDN);
            public static DN GetOnBehalfOf(ActiveConnection ac, DN owner);
            public static string ToCallerStr(List<ActiveConnectionFields> theList);
            public static string ToCallerStr(ActiveConnectionFields ac);
            public static List<string> ToCallerStrArray(List<ActiveConnectionFields> theList);
            public static DnType TypeOfDN(DN dn);
            public string BuildOtherPartyDisplayName(ActiveConnection otherPartyConnection, DN otherPartyDN, string originatorname);
            public ConnectionState GetConnectionState(ActiveConnection ac);
            public string GetPBContacts(ActiveConnection ac, DN theDN);
            public override string ToString();

            public enum CallState
            {
                Unknown = 0,
                Initiating = 1,
                Routing = 2,
                Talking = 3,
                Transferring = 4,
                Rerouting = 5
            }

            public class ConnectionState
            {
                public ConnectionState();

                public string OtherPartyCallerId { get; }
                public bool Recording { get; }
                public string OwnerDeviceContact { get; }
                public int Capabilities { get; }
                public bool SLABreach { get; }
                public string OwnerCallerId { get; }
                public string OwnerDisplayName { get; }
                public string OwnerDn { get; }
                public DnType OwnerType { get; }
                public DateTime? AnsweredAt { get; }
                public DateTime? StartedAt { get; }
                public bool IsIncoming { get; }
                public string OtherPartyCompanyContactRefs { get; }
                public string OtherPartyDisplayName { get; }
                public string OtherPartyDn { get; }
                public DnType OtherPartyType { get; }
                public string OriginatorName { get; }
                public string OriginatorDn { get; }
                public DnType OriginatorType { get; }
                public LocalConnectionState? State { get; }
                public int LegId { get; }
                public int CallId { get; }
                public int Id { get; }
                public DN OnBehalfOf { get; }

                public override string ToString();
            }
        }

Here is an example of a call data using the ToString() method.

ID=573948
State=Routing
Owner=ac=AttachedData=System.Collections.Generic.Dictionary`2[System.String,System.String]
LastChangeStatus=2/30/2019 8:08:39 PM
IsOutbound=True
IsInbound=False
DialedNumber=
ExternalParty=987656774
InternalParty=
Status=Dialing
HistoryIDOfTheCall=00000FGHDDFD6B00A0_573948
OriginatedBy=
ReferredBy=0
CallConnectionID=1
CallID=14458
OnBehalfOf=
DN=Wextension.56: 1003
OtherCallParties=TCX.Configuration.Interop.Wconnection[]
RecordingState=Stop
    AttachedData:sip_displayname=some name some name
        lookup_displayname=some name some name
        tag3cx=6ac0f57c-df89-fdfg-dfdf-bdf4ff3341c9
        chid=00000FGHDDFD6B00A0_573948
        prevCall=0
        prevLeg=0
        sip_dialog_set_id=jjf8jf8fjeje-548ff8fds-97db-jewf8ewf7ef878f8ds
        extnumber=76849499939
        devcontact=sip:1003@127.0.0.1:port;rinstance=8-473757859ec-jjsdks-

dn=Wextension.560: 1003
dnhash=560
onbehalf=0

Destination=76849499939
RoutingTo=System.Collections.Generic.List`1[TCX.Configuration.OMCallCollector+ActiveConnectionFields]
TalkTo=System.Collections.Generic.List`1[TCX.Configuration.OMCallCollector+ActiveConnectionFields]
Bargein=System.Collections.Generic.List`1[TCX.Configuration.OMCallCollector+ActiveConnectionFields]
Transferror=
StartedAt=2/30/2019 8:08:39 PM
AnsweredAt=
Participants=System.Collections.Generic.HashSet`1[System.Int32]

I have written some codes to parse this string and derive all the properties I needed. Key issues with 3cX data is that sometimes data are not consistent.

For example, incoming calls can sometimes have IsOutbound=True and IsInbound=False which is ridiculous. Other times, the Extension number or the destination number might be missing. This calls for further processing of the RoutingTO, TalkTo and Participants properties to get that information.

I have implemented many codes to solve these. If you need them, email me directly and let's have a discussion.

Enjoy!