Articles in the "Lync call handling script" series
- Trigger something whenever you’re on a Lync call [This article]
- Update to Lync call handling script
- Lync call handling script updated
A coworker of mine works out of his home office and wanted his family to know when he is on a phone call so they won’t interrupt him. He already had a network-controllable light that he put into the hallway outside the office. What he needed was a way to trigger the light when he is on the phone (he is enterprise voice-enabled in Lync). Here are the details of the PowerShell script that I wrote for him.
The script leverages the API in the Lync SDK. You only need to install the Lync SDK runtime library, but there isn’t an individual download for that. To install the Lync SDK, however, requires that Visual Studio is installed. That may not be the case for many of you, so I have included the redistributable runtime library (a mere 750KB MSI) in the script download.
The runtime library installs the assemblies into the global access cache (GAC), so to use one of them in a script, you need to load the assembly:
1 2 |
$apiPath = "C:\Windows\assembly\GAC_MSIL\Microsoft.Lync.Model\4.0.0.0__31bf3856ad364e35\Microsoft.Lync.Model.dll" Add-Type -Path $apiPath |
You can then create an object for the Lync client:
1 |
$client = [Microsoft.Lync.Model.LyncClient]::GetClient() |
In order to know someone’s current presence state (or activity in Lync parlance), you work against a contact object. Therefore, to know your own activity, you first need to create an object for yourself as a contact:
1 |
$selfContact = $client.Self.Contact |
To get your current activity, you use the GetContactInformation() method, with the argument being the type of information to retrieve:
1 |
$selfContact.GetContactInformation([Microsoft.Lync.Model.ContactInformationType]::Activity) |
The activities that indicate you are on the phone, or off-hook in telecom parlance, are In a call and In a conference call. To use this information to do something, you need to know when Lync changes to and/or from these activities, and for that you can use .NET Framework object events. Instead of constantly querying for your activity, you can use events to have PowerShell act when something happens. The Contact class has an event for when contact information changes, and here is the list of what properties will result in that event firing.
To register for an event, you have to include the object whose class contains the event, as well what action to take:
1 |
Register-ObjectEvent -InputObject $selfContact -EventName "ContactInformationChanged" -SourceIdentifier "OffHookHandler" -Action {Offhook-Handler $event} |
$event is one of the built-in variables that the event can include in the response. And this is the function that will run as a result of the event firing:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
function Offhook-Handler ($event) { #Act if what has changed is activity if ($event.SourceEventArgs.ChangedContactInformation -contains 'Activity') { $newActivity = $selfContact.GetContactInformation([Microsoft.Lync.Model.ContactInformationType]::Activity) #Act only on true activity change if ($newActivity -ne $currentActivity) { #Act if off- or on-hook if ($newActivity -eq 'In a call' -or $newActivity -eq 'In a conference call') { if ($offhook -eq $false) #Only run off-hook action if not already on a call { Write-VerboseEvent $newActivity OffHook-Action 'offhook' $global:offhook = $true #Stateful tracking of status in successive changes } } else { if ($offhook) { OffHook-Action 'onhook' Write-VerboseEvent "No longer on the phone ($newActivity)" $global:offhook = $false } else { Write-VerboseEvent "Non-phone activity change: $newActivity" } } #Global variable provides stateful tracking of activity change $global:currentActivity = $newActivity } } } |
Since the list of changed properties that triggers a changed contact event is numerous, the first thing the function does is check to see if the changed property is Activity. The event doesn’t include what the activity is, so you have to get the current activity. If the activity is one of the values that indicates you are on the phone, it calls a function to run your code that does something. (I did it this way, instead of having you put your code inside this function, so that it is easier to paste your code into a function at the beginning of the script and for doing so if there are future versions of the script.) A global variable of $offhook is set. This allows the script to keep track of whether you are off the phone, after having been on the phone, in subsequent event firings. If you are no longer on the phone, after having been on the phone, it calls another function to run your code that “undoes” whatever it did when you got on the phone.
The script will normally not output anything to the screen, allowing you to run it in a shell you are using for something else. If you are testing your code or just want to see more information about your activity changes, you can include the -Verbose parameter. This will result in more details being output to the screen as they occur, such as current activity, functions being called, and whether the client is signed in. If you do this, you will likely want to run the script in its own shell so it doesn’t get in the way of what you might be doing in the first shell.
Now, to put all of this into a reusable script, there are other things to consider. If you sign out, or get otherwise disconnected from Lync, the contact object is no longer valid and has to be created again when you sign in. To account for this, I use another event that fires on client state changes. If not signed in, it unregisters the contact change event, and when you get signed in again it creates the contact object and registers the event.
Another consideration is the scope of the script. When you run a script, its code is run in its own scope, terminating when the script reaches the end. Since the registered events will call functions after the script has completed, they need to be run in a scope that stays resident upon completion, i.e., global scope. To do this, you dot-source the script by entering a period and a space before the path to the script. To account for this, the script checks to ensure that it was dot-sourced when executed, otherwise it presents an error.
As for using the -Verbose parameter, I found that it worked fine when using Write-Verbose in the script’s main block and initial functions. It did not work in the functions called by the events. The function would just hang at the point of running the cmdlet. I have not been able to find out why this is the case, so I wrote a function to mimic the output of the Write-Verbose cmdlet. This function is used in the functions called by the events.
To make use of this script in your environment, you will want to modify the first function, called OffHook-Action, by replacing lines 13 and 19 with code to “do” something and “undo” something, respectively. If those lines of code are dependent on other functions, you can put them in the subsequent region that is labeled for your dependent functions.
As a side note, since I don’t have a network-controllable light at work, I needed something else for it to do so I could be sure it was running correctly. I use Winamp, so it was appropriate for me to have it pause when I am on the phone, and resume if I am off the phone (but only if it is already paused). The code in the script, therefore, includes functions for controlling Winamp and reference to a COM object the provides this. I left it in so you can see an example of the custom code that you will need to put in your version. It is worth nothing that the COM object for controlling Winamp is a 32-bit library. If you are running PowerShell on a 64-bit system and reference a library that is only compiled for a 32-bit system, you will need to run the x86 version of PowerShell (there is a Start Menu item for it). (The Lync SDK runtime is accessible in both versions of the shell.)
The download of the full version of the script (and the Lync SDK runtime library) is below, but you can also see the full script by expanding the code block below:
Monitor-LyncSelfActivityChange.zip (640.5 KiB)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 |
#Take an action when Lync presence indicates you are on a phone call #v1.4.4 5/30/13 [CmdletBinding()] param() #Put your custom code in this function in response to being on or off the phone function OffHook-Action ($action) { if ($action -eq 'offhook') { #Do something Pause-Winamp Write-VerboseEvent "Off-hook action executed" } else { #Undo something Play-Winamp Write-VerboseEvent "On-hook action executed" } } #Region Your dependent function(s) for off-hook actions $winamp = New-Object -ComObject ActiveWinamp.Application function Play-Winamp { #Play only if Winamp currently paused if ($winamp.PlayState -eq 3) { $winamp.Play() } } function Pause-Winamp { #Pause only if Winamp currently playing if ($winamp.PlayState -eq 1) { $winamp.Pause() } } #EndRegion #Region Core functions function Write-VerboseEvent ($text) { if ($verboseEvent) { Write-Host "VERBOSE: $((Get-Date).ToLongTimeString()) - $text" -ForegroundColor Yellow -BackgroundColor Black } } function Connect-LyncClient { $i = 1 do { #Attach to Lync process, if running #Choose 'lync' line for 2013, 'communicator' line for 2010 $lyncProcess = [System.Diagnostics.Process]::GetProcessesByName('lync') #$lyncProcess = [System.Diagnostics.Process]::GetProcessesByName('communicator') if ($lyncProcess.Length -eq 0) #Process is not running { if ($i -eq 1) #Report status only on first attempt { Write-Host "$((Get-Date).ToLongTimeString()) - Waiting for Lync process to start (15-second intervals)..." -ForegroundColor Yellow } $i++ Start-Sleep 15 } } until ($lyncProcess.Length -eq 1) #Register for when Lync process exits Register-ObjectEvent -InputObject $lyncProcess[0] -EventName "Exited" -SourceIdentifier "LyncProcessHandler" -Action {LyncProcess-Handler} | Out-Null #Wait for client object initialization to complete do { $global:client = [Microsoft.Lync.Model.LyncClient]::GetClient() } while (-not $client -or $client.State -eq [Microsoft.Lync.Model.ClientState]::Invalid) } function Register-ContactChange { #Create self object as a contact $global:selfContact = $client.Self.Contact #Register for contact changes Register-ObjectEvent -InputObject $selfContact -EventName "ContactInformationChanged" -SourceIdentifier "OffHookHandler" -Action {Offhook-Handler $event} | Out-Null #Get initial off-hook status and set state variable $activity = $selfContact.GetContactInformation([Microsoft.Lync.Model.ContactInformationType]::Activity) if ($activity -eq 'In a call' -or $activity -eq 'In a conference call') { $global:offhook = $true } else { $global:offhook = $false } } function Register-ClientStateChange { #Register for sign-in changes Register-ObjectEvent -InputObject $client -EventName "StateChanged" -SourceIdentifier "ClientStateHandler" -Action {ClientState-Handler $event} | Out-Null } function Offhook-Handler ($event) { #Act if what has changed is activity if ($event.SourceEventArgs.ChangedContactInformation -contains 'Activity') { $newActivity = $selfContact.GetContactInformation([Microsoft.Lync.Model.ContactInformationType]::Activity) #Act only on true activity change if ($newActivity -ne $currentActivity) { #Act if off- or on-hook if ($newActivity -eq 'In a call' -or $newActivity -eq 'In a conference call') { if ($offhook -eq $false) #Only run off-hook action if not already on a call { Write-VerboseEvent $newActivity OffHook-Action 'offhook' $global:offhook = $true #Stateful tracking of status in successive changes } } else { if ($offhook) { OffHook-Action 'onhook' Write-VerboseEvent "No longer on the phone ($newActivity)" $global:offhook = $false } else { Write-VerboseEvent "Non-phone activity change: $newActivity" } } #Global variable provides stateful tracking of activity change $global:currentActivity = $newActivity } } } function ClientState-Handler ($event) { #Get current client state $newState = $event.SourceEventArgs.NewState if ($newState -eq 'SignedIn') { Register-ContactChange Write-VerboseEvent "Activity changes now being monitored." } elseif ($newState -eq 'SignedOut') { $subscriptionSource = Get-EventSubscriber | Select-Object -ExpandProperty SourceIdentifier if ($subscriptionSource -contains "OffHookHandler") { #If subscription currently is registered, remove it so it can #be successfully created again when signed in Unregister-Event OffHookHandler Write-VerboseEvent "Activity changes will be monitored when the client signs in." } } } function LyncProcess-Handler { Write-Host "$((Get-Date).ToLongTimeString()) - Lync client has shut down." -ForegroundColor Yellow #Client object is invalid if Lync process stops Stop-Monitoring #Restart connection and registration steps Connect-LyncClient Initialize-Registration } function Initialize-Registration { #Register for contact changes if client is already signed in if ($client.State -eq [Microsoft.Lync.Model.ClientState]::SignedIn) { Register-ContactChange Write-Host "$((Get-Date).ToLongTimeString()) - Activity changes now being monitored." -ForegroundColor Green Register-ClientStateChange } #Register for client changes, which will handle contact change registration else { Register-ClientStateChange Write-VerboseEvent "Activity changes will be monitored when the client has signed in." } } function Stop-Monitoring { Unregister-Event OffHookHandler -ErrorAction SilentlyContinue Unregister-Event ClientStateHandler -ErrorAction SilentlyContinue Unregister-Event LyncProcessHandler -ErrorAction SilentlyContinue Write-VerboseEvent "Events unregistered" $global:client = $null $global:lyncProcess = $null $global:currentActivity = $null } #EndRegion #Region Script body #Check for dot sourcing if ($MyInvocation.InvocationName -ne '.') { Write-Error "Script was not dot-sourced. This script is designed to be executed by dot sourcing it: . <pathtoscript>" -Category InvalidOperation break } #Check for SDK installation $apiPath = "C:\Windows\assembly\GAC_MSIL\Microsoft.Lync.Model\4.0.0.0__31bf3856ad364e35\Microsoft.Lync.Model.dll" if (Test-Path $apiPath) { Add-Type -Path $apiPath #Connect to local Lync client Connect-LyncClient } else { Write-Error "This script requires the Lync SDK runtime library." -Category NotInstalled break } #Check for Verbose parameter for event functions if ($MyInvocation.BoundParameters['verbose']) { $global:verboseEvent = $true } else { $global:verboseEvent = $false } #Start event registration Initialize-Registration #EndRegion |
hi,
i am trying your code and once i have run the script, it seems that it is stuck with “Waiting for Lync process to start (15-second intervals)…” any idea?
thanks!
Make sure you have the appropriate line (62 or 63) commented for use with Lync 2013 or with Lync 2010/Communicator, respectively.