A couple weeks ago, during an internal training, it was given to me an awesome challenge: create an interface that is able to store Lync's statuses from a given user. People here at π² often ask about Lync information, so I'm writing this post to share this with you. First of all, you'll need to install Lync's SDK. You can get it here. You will also need the latest version of AF SDK, but if you are reding this, most likely you have it alredy. The code was done using C# .NET 4.5 and Visual Studio 2015.

 

Disclaimer: as you will see during this post, this code is not exactly an interface, but considering that it lists points from the server, gets data from a data source and write it back to the data archive, we can say it qualifies as one.

 

1. Lync SDK

 

The SDK works pretty good and I'm very surprised to see that there are tons of examples available online on how to use it. For this exercise, we will only need to import one assembly from the SDK: Microsoft.Lync.Model. If you are struggling to find it, the default installation is at C:\Program Files (x86)\Microsoft Office\Office15\LyncSDK\Assemblies\Desktop\.

 

So, refer and import it:

 

using Microsoft.Lync.Model;

 

Now you have access to the two most important classes for this exercise: LyncClient and Contact. They work exactly as the Lync application works: you have to instantiate a client and use it to get a contact. There are some catches, but I will talk about them later.

 

Now, before starting, you actually need a Lync application running on your dev machine. I will explain better later, but essentially what the SDK does is to expose the running client as an API.

 

2. Getting statuses

 

Ok, so now we are ready to get statues. First let me explain on thing regarding contacts.

 

Contacts have a unique URI identifier and that's how you find them. I could not found resources on this, but apparently a default Lync install will use a format like "sip:email@server.com". It's a well known protocol called Session Initiation Protocol. To check whether yours follow this standard is actually pretty simple:

 

LyncClient client = LyncClient.GetClient();
string uri = client.Self.Contact.Uri;

 

I guess it does, but just in case...

 

So, to get the status is dead simple. Check it out:

 

private static string GetLyncStatus(string email)
{
  try
  {
    // Instantiating a client
    LyncClient client = LyncClient.GetClient();

    // Checking if the user is signed in
    if (client.State == ClientState.SignedIn)
    {
      // Getting information from the user
      var contact = client.ContactManager.GetContactByUri("sip:" + email);

      // Does it exist?
      if (contact != null)
      // Finally getting what we need!
        return contact.GetContactInformation(ContactInformationType.Activity).ToString();
      else
        return "Error: No user found";
    }
    else
    {
      return "Error: Invalid Client Status";
    }
  }
  catch
  {
    return "Error: Server Error";
  }
}

 

See how simple it is? You just instantiate a client, see if it's logged, search for a contact and get its status.

 

3. Storing Data using AF SDK

Ok, now we know how to get the data we need, but how to store it in a tag? If you know how to do this, please skip to the next topic. If you don't, let me quickly guide you through the code.

 

First we need to import some libraries from the AF SDK:

 

using OSIsoft.AF.PI;
using OSIsoft.AF.Search;
using OSIsoft.AF.Asset;
using OSIsoft.AF.Data;
using OSIsoft.AF.Time;

 

Now our code must get a connection to the PI Server.

 

PIServers piServers = new PIServers();
PIServer piServer = piServers["PI_SERVER_NAME"];
piServer.Connect();

 

For the sake of this exercise, I have created some tags using a point source called LYNC and with the user's email stored as source tag.

 

lynctag.png

Back to our code, now that we are connected, we need to list all tags under this LYNC point source. The trick here is that we also have to specify the properties we want to load.

 

// Defining the filter
PIPointQuery nameFilter = new PIPointQuery
{
  AttributeName = PICommonPointAttributes.PointSource,
  AttributeValue = "LYNC",
  Operator = AFSearchOperator.Equal
};

// Defining the attributes to load
IEnumerable<string> attributesToLoad = new[]
{
  PICommonPointAttributes.SourceTag
};

// List what we need
IEnumerable<PIPoint> points = PIPoint.FindPIPoints(piServer, new[] { nameFilter }, attributesToLoad);

 

Easy, isn't it? The next step is get the status for each point we have and store. The code is a little bigger, but very simple:

 

// List that will store all new values
IList<AFValue> valuesToWrite = new List<AFValue>();

// For each point...
foreach (PIPoint point in points)
{
  // Getting the email from the source tag
  var email = point.GetAttribute(PICommonPointAttributes.SourceTag).ToString().Trim();

  // Checking if it's empty
  if (email != "")
  {
    // Getting the current Lync status 
    string currentStatus = GetLyncStatus(email);

    // We will store only if the previous value differs from the current
    if (point.CurrentValue().Value.ToString() != currentStatus)
    {
      // Getting the current timestamp
      AFTime time = new AFTime(DateTime.Now);

      // Mixing up with the status so we can get an AFValue
      AFValue value = new AFValue(currentStatus, time);
      value.PIPoint = point;
      value.IsGood = currentStatus.Contains("Error") ? false : true;
      valuesToWrite.Add(value);
      LogInfo("New value found: " + currentStatus);
    } 
    else
    {
      LogInfo("No status change");
    }
  }
  else
  {
  LogInfo("Tag " + point.Name + " has no email configured on InstrumentTag");
  }
}

 

Well, now we have the variable valuesToWrite filled with new values. To push it to the Data Archive is very simple:

 

piServer.UpdateValues(valuesToWrite, AFUpdateOption.InsertNoCompression, AFBufferOption.BufferIfPossible);

 

And that's all! Here is what I got from some colleagues here at OSISoft:

 

lyncStatus.png

There are some improvements that are actually simple to implement and I will leave them as a challenge to you (feel free to get in touch if you need help with one of them!):

  • Use a classic point and get Location4 information to set a loop simulating scan classes
  • Create digital states and store data as a state, not a string
  • Manage data security
  • Improve error handling

 

The full project I've created is attached (Lync2PI.zip).

 

4. Catches

 

As I mentioned earlier, the Lync SDK works by exposing a local running client. That may be a problem as, sometimes, local IT policies do not allow client applications running on server, but that's the best way to make it work.

 

Also, as you can see above, some users got a status "presence unknown". The reason is bizarre yet understandable: the only way to make sure the user will have its status registered, is to actually have the contact added as a friend in your Lync client. I could not find a way to solve this issue, but as soon as I find one I will update this article.

 

5. Bonus: Sending Data back to Lync

 

This is fun! I will extend the article a little and show how to use Lync's SDK to send messages. Just to make VB.NET folks happy, I will write this code using VB. But why would you send data back to Lync? To send messages! Imagine an application that keeps listening to the tags and warn a manager when an employee has been away for longer than expected (please don't hate me).

 

I will not explain the code as I did earlier, by now I guess it will be easy for you to understand what it does.

 

Private Sub SendLyncMessage(address As String, message As String)
  'Getting a client the same way we did before
  Dim client As LyncClient
  client = LyncClient.GetClient()
  If (client.State = ClientState.SignedIn) Then
    Dim contact As Contact
    contact = client.ContactManager.GetContactByUri("sip:" + address)
    'Now the fun beggins...
    'First you start by creating a new conversation
    Dim conversation As Conversation.Conversation
    conversation = client.ConversationManager.AddConversation()
    'Then you add the recipient of the message
    conversation.AddParticipant(contact)
    'Now we have to say that this is a text message
    Dim modality As Microsoft.Lync.Model.Conversation.InstantMessageModality
    modality = conversation.Modalities(Microsoft.Lync.Model.Conversation.ModalityTypes.InstantMessage)
    'Callback time as the communication is async
    Dim callBack As AsyncCallback
    callBack = AddressOf SendMessageCallback
    'Sending message...
    modality.BeginSendMessage(message, callBack, modality)
    'Ending conversation so you don't end up with a memory allocation
    conversation.End()
  End If
End Sub

Shared Sub SendMessageCallback(ar As IAsyncResult)
  'Here we the message that was originally sent
  Dim modality As Microsoft.Lync.Model.Conversation.InstantMessageModality
  modality = CType(ar.AsyncState, Microsoft.Lync.Model.Conversation.InstantMessageModality)
  Try
    'And now we try to close it
    modality.EndSendMessage(ar)
  Catch ex As Exception
  End Try
End Sub

 

That's all for today. I hope you like this.

 

'Til next time and happy coding!