Microsoft Teams Presence IOT Device on a Raspberry Pi Zero W

Firstly this is not a step by step setup. Just an overview of the setup that may help someone already familiar with many of these topics. The journey started with an idea to learn Microsoft Teams MS Graph API and use a raspberry pi zero w with tri-colour LED to integrate and be self standing. I thought it would be easy but then we think many things are easy until you try. Finally success.

This proof of concept does not use very many external libraries as they normally are too complicated or bloated and I did not require all the features just to learn how to sync MS Teams Presence for a user with the cloud API of MS Graph and Azure. One thing to note is this relies on setting up an Application API in Azure so if you dont have admin access you may struggle to fully implement this. Anyways hopefully this code and screenshot saves someone some time.

Requires:

  • Python 3.7 (well thats what I used)

  • Raspberry pi Zero W with GPIO configured for a Tri-LED (RGB - Red Green Blue)

  • O365 Test tenant or equivalent

  • Very basic knowledge of MS Graph API, MS Teams, Azure AD etc.

Create an Application in Azure for the IOT Device (Raspberry PI Zero W):

Use device code flow for devices that dont run windows and cannot sign into win10 /ms teams themselves. i.e. an IOT device. Basically uses a web browser and code to authorise initial access token and then ongoing the refresh token is used to get new access tokens.

for presence and to utilise refresh tokens, the highlighted options are needed below. offline_access is required if you want to see refresh_tokens show up when a token is requested.

Then the python script (which has notes identifying how its put together) is run to get an access token, then refresh token, then update the GPIO of LEDs after querying the presence status in the MS Graph API. Note code below has some debug hashed out. Hopefully helps someone who does not want to deploy MSAL and all the python libraries but still get an understanding of device code flow for IOT.

#!/usr/bin/env python3

#$VERSION = ms_teams_presence_api_20200426_1.3

# script by Andrew Fullagar http://blog.fullys.xyz

#coding: utf-8

###

### - MS Teams presence api - MS Teams Prototype - Manual Busy Lamp or API driven

###


#Pre-Reqs

#sudo apt-get install python-rpi.gpio python3-rpi.gpio

#======================================================


import time #used for sleep

import datetime #used to calc Now time

import pathlib #Used to create dirs

import RPi.GPIO as GPIO #used to control GPIO pins


import requests #http requests

import json


i = 0

greenLedGpio = 19

blueLedGpio = 20

redLedGpio = 21


sleepTimer = 1 #in seconds


GPIO.setmode(GPIO.BCM) # set board mode to Broadcom


GPIO.setup(greenLedGpio, GPIO.OUT) # set up pin 35

GPIO.setup(blueLedGpio, GPIO.OUT) # set up pin 38

GPIO.setup(redLedGpio, GPIO.OUT) # set up pin 40


#log file paths

logPath = '/home/pi/busy_light/logs/'


#o365.fullys.xyz_tenant_ID / fullys.onmicrosoft.com = afe461aa-be2c-474f-b755-ff1969e45458

#this function drives the device flow to get access and refresh tokens based device code for remote browser signin

#OAuthv2 - https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-device-code

#NOTE - Refresh token will only be provided in Application API permissions has offline_access added in AzureAD - Very NB for continued functioning of IOT device

def getMSDeviceFlowAuth():

tokenType=" "

expiresIn=0

jsonDict = 0

expiry=0

presenceCheck=0

refreshToken=0

print("Start to get remote Device Flow Authentication - MS Access Token via Azure Delegated API App.......")

#Tenant ID of O365 in numeric

tenantIDNumeric = 'afe461aa-be2c-474f-b755-ff1969e45458'

#Tenant ID of O365 in alphanumeric

tenantIDAlphanumeric = 'fullys.onmicrosoft.com'

#define what can be accessed

###scope='offline_access%20user.read'

scope='offline_access Presence.Read Presence.Read.All User.Read.All'

#OAuth v2 registered endpoint

requestUrlAuth = 'https://login.microsoftonline.com/'+ tenantIDAlphanumeric

#Application ID in Azure - required for both delegated and application access

client_id = 'a573e36e-96da-488f-abd4-26014e88bfcf-bogus-for-blog'

payloadDeviceCode = {'client_id':'a573e36e-96da-488f-abd4-26014e88bfcf-bogus-for-blog','Scope':scope}

#Device Code request to start browser sign in auth for access and refresh token

requestDeviceCodeUrl = 'https://login.microsoftonline.com/'+tenantIDNumeric+'/oauth2/v2.0/devicecode'

r = requests.get(requestDeviceCodeUrl, data=payloadDeviceCode)


#Debug - print out request

print(r.text)

jsonDict = json.loads(r.text)

expiresIn = jsonDict['expires_in']

print(jsonDict['message'])

print(jsonDict['verification_uri'])

#user code used for input to ms website for access token

userCode = jsonDict['user_code']

#Debug

print("user_code=",userCode)

#user code used for input to ms website for access token

deviceCode = jsonDict['device_code']

#Debug

print("device_code=",deviceCode)

#Try every 900s / 15 minutes

print(jsonDict['expires_in'])

print(jsonDict['interval'])


#this grant type is requird for device code requests

grantType = 'urn:ietf:params:oauth:grant-type:device_code'


#debug

#access token expires in 3359 seconds or until a refresh token is used.

#Start at 1 instead of zero

i=1

for i in range(expiresIn):

print(i)

time.sleep(5)

#print("totenType=" + tokenType)

try:

print("lets get a token......!")

payloadGetToken = {'client_id':'a573e36e-96da-488f-abd4-26014e88bfcf-bogus-for-blog','grant_type':grantType,'device_code':deviceCode}

r = requests.post('https://login.microsoftonline.com/afe461aa-be2c-474f-b755-ff1969e45458/oauth2/v2.0/token', data=payloadGetToken)

#payload = {'client_id':'a573e36e-96da-488f-abd4-26014e88bfcf-bogus-for-blog','scope':'User.Read','client_secret':'ebd7@Sd8lPcF6F0xL]XccvnEGBFo@K_z','grant_type':'client_credentials'}

jsonDict = json.loads(r.text)

print(r.text)

#r = requests.post('https://login.microsoftonline.com/o365.fullys.xyz/oauth2/v2.0/token', data=payload)

try:

errorAuthPending = jsonDict['error']

print("Still waiting for access token.............................................", errorAuthPending)

except:

accessToken = jsonDict['access_token']

refreshToken = jsonDict['refresh_token']

print(accessToken)

print("Refresh Token before passing down to other routine (original)="+str(refreshToken))

tokenType = jsonDict['token_type']

expiresIn = jsonDict['expires_in']

#generate refresh token for below access token refresh after 3359

print("token_type="+tokenType)

print("Start MS Teams Presence gather.......")

#Needs to be refreshed often due to MFA etc.

#access_token = 'eyJ0eXAiOiJKV1QiLCJub25jZSI6Il90RnRUSUtOalpLWWg1MU8xemtZaDVwd0o3LTVxcmt3YjE3WlR4R2lkZ00iLCJhbGciOiJSUzI1NiIsIng1dCI6IkN0VHVoTUptRDVNN0RMZHpEMnYyeDNRS1NSWSIsImtpZCI6IkN0VHVoTUptRDVNN0RMZHpEMnYyeDNRS1NSWSJ9.eyJhdWQiOiJodHRwczovL2dyYXBoLm1pY3Jvc29mdC5jb20iLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC9hZmU0NjFhYS1iZTJjLTQ3NGYtYjc1NS1mZjE5NjllNDU0NTgvIiwiaWF0IjoxNTg3NjUwOTE4LCJuYmYiOjE1ODc2NTA5MTgsImV4cCI6MTU4NzY1NDgxOCwiYWlvIjoiNDJkZ1lQZ2RMSGhyMVQvL1E5WXBZZGUvM0luZUNBQT0iLCJhcHBfZGlzcGxheW5hbWUiOiJBY3Rpdml0eV9TdGF0dXNfUHlBcm1JVCIsImFwcGlkIjoiYTU3M2UzNmUtOTZkYS00ODhmLWFiZDQtMjYwMTRlODhhZWJlIiwiYXBwaWRhY3IiOiIxIiwiaWRwIjoiaHR0cHM6Ly9zdHMud2luZG93cy5uZXQvYWZlNDYxYWEtYmUyYy00NzRmLWI3NTUtZmYxOTY5ZTQ1NDU4LyIsIm9pZCI6IjljZGNkYThmLWIyZGQtNDM5OS1hODQ1LWFlOTBlOGQ2ZTI3ZiIsInJvbGVzIjpbIlVzZXIuUmVhZC5BbGwiXSwic3ViIjoiOWNkY2RhOGYtYjJkZC00Mzk5LWE4NDUtYWU5MGU4ZDZlMjdmIiwidGlkIjoiYWZlNDYxYWEtYmUyYy00NzRmLWI3NTUtZmYxOTY5ZTQ1NDU4IiwidXRpIjoiaUtxbS1ULW1iVVdRSVJmenRRZ3FBQSIsInZlciI6IjEuMCIsInhtc190Y2R0IjoxNTczNTYyMjkyfQ.yFC6XzISPqtGHd2reEwqpc701a1KU1JiAeWBjY0Ngw_rM0d8Pw2snDXZOCdIeRsb5EmZwUCMwf0fkoFcyhnRDhTMlmyrIsQyn_Gl5Tfdq7Mc73s0neQshVlSZmhHmZz3MJ-x9mrgGcHagM8FW21eLekYcbeOIKEG-XV7o-EdKq0ADrCwdDOxF3NR0XhNjDoT6kxl3Nh72eYLmoxZEjTjlER6zO1YO4PaW5ZoEAQ9mr8Y91oMN_an9joSHx6RyhGcYVRh7MMvpb2u1uUBvIPO2qCjb8btlh1rbETHBVmp3J9ffYUWGth27SqQxn1g-G3tCgQZUWMxc8g-kLvAmJO2cQ'

presenceUrl = 'https://graph.microsoft.com/beta/users/892d512e-0bd5-455b-94b2-aeb47eb78a07-bogus-user-for-blog/presence'

#presenceUrl = 'https://graph.microsoft.com/v1.0/users/892d512e-0bd5-455b-94b2-aeb47eb78a07-bogus-user-for-blog'

r = requests.get(presenceUrl,headers={'Authorization': 'Bearer {}'.format(accessToken)})

print(r.headers)

print(r.status_code)

print(r.json())

#set i to 1 higher than expiresIn so it exits main for loop as well

i = expiresIn+1

#Debug

print("New i value expiresIn+1=",i)

#exit out of loop

break

except Exception as e:

print("type error: " + str(e))

#print(traceback.format_exc())

pass

#refresh token to get new access token logic

try:

while expiry <= 3600:

presenceCheck = 0

expiry=expiry+1

#Use refresh token to keep going

grantType='refresh_token'

print("Previous Refresh Token="+str(refreshToken))

print("lets get a refresh token and then get an access token......!")

payloadGetFromRefreshGetAccessToken = {'client_id':'a573e36e-96da-488f-abd4-26014e88bfcf-bogus-for-blog','grant_type':grantType,'refresh_token':refreshToken,'scope':scope}

r = requests.post('https://login.microsoftonline.com/afe461aa-be2c-474f-b755-ff1969e45458/oauth2/v2.0/token', data=payloadGetFromRefreshGetAccessToken)

#payload = {'client_id':'a573e36e-96da-488f-abd4-26014e88bfcf-bogus-for-blog','scope':'User.Read','client_secret':'fee7@Sd8lPcF6F0xL]XddvnEGBFo@K_z','grant_type':'client_credentials'}

print(r.text)

#r = requests.post('https://login.microsoftonline.com/o365.fullys.xyz/oauth2/v2.0/token', data=payload)

jsonDict = json.loads(r.text)

print(jsonDict['access_token'])

accessToken = jsonDict['access_token']

tokenType = jsonDict['token_type']

expiresIn = jsonDict['expires_in']

#refreshToken = jsonDict['refresh_token']

#print("New refresh token="+str(refreshToken))

#print("token_type="+tokenType)

presenceCheckFrequency = 5 #seconds

presenceCheckMax = expiresIn/presenceCheckFrequency

while presenceCheck <= presenceCheckMax:

presenceCheck=presenceCheck+1

time.sleep(presenceCheckFrequency)

print("Start MS Teams Presence gather every ",presenceCheckFrequency," seconds..........Checked ", presenceCheck, "times")

#Needs to be refreshed often due to MFA etc.

#access_token = 'eyJ0eXAiOiJKV1QiLCJub25jZSI6Il90RnRUSUtOalpLWWg1MU8xemtZaDVwd0o3LTVxcmt3YjE3WlR4R2lkZ00iLCJhbGciOiJSUzI1NiIsIng1dCI6IkN0VHVoTUptRDVNN0RMZHpEMnYyeDNRS1NSWSIsImtpZCI6IkN0VHVoTUptRDVNN0RMZHpEMnYyeDNRS1NSWSJ9.eyJhdWQiOiJodHRwczovL2dyYXBoLm1pY3Jvc29mdC5jb20iLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC9hZmU0NjFhYS1iZTJjLTQ3NGYtYjc1NS1mZjE5NjllNDU0NTgvIiwiaWF0IjoxNTg3NjUwOTE4LCJuYmYiOjE1ODc2NTA5MTgsImV4cCI6MTU4NzY1NDgxOCwiYWlvIjoiNDJkZ1lQZ2RMSGhyMVQvL1E5WXBZZGUvM0luZUNBQT0iLCJhcHBfZGlzcGxheW5hbWUiOiJBY3Rpdml0eV9TdGF0dXNfUHlBcm1JVCIsImFwcGlkIjoiYTU3M2UzNmUtOTZkYS00ODhmLWFiZDQtMjYwMTRlODhhZWJlIiwiYXBwaWRhY3IiOiIxIiwiaWRwIjoiaHR0cHM6Ly9zdHMud2luZG93cy5uZXQvYWZlNDYxYWEtYmUyYy00NzRmLWI3NTUtZmYxOTY5ZTQ1NDU4LyIsIm9pZCI6IjljZGNkYThmLWIyZGQtNDM5OS1hODQ1LWFlOTBlOGQ2ZTI3ZiIsInJvbGVzIjpbIlVzZXIuUmVhZC5BbGwiXSwic3ViIjoiOWNkY2RhOGYtYjJkZC00Mzk5LWE4NDUtYWU5MGU4ZDZlMjdmIiwidGlkIjoiYWZlNDYxYWEtYmUyYy00NzRmLWI3NTUtZmYxOTY5ZTQ1NDU4IiwidXRpIjoiaUtxbS1ULW1iVVdRSVJmenRRZ3FBQSIsInZlciI6IjEuMCIsInhtc190Y2R0IjoxNTczNTYyMjkyfQ.yFC6XzISPqtGHd2reEwqpc701a1KU1JiAeWBjY0Ngw_rM0d8Pw2snDXZOCdIeRsb5EmZwUCMwf0fkoFcyhnRDhTMlmyrIsQyn_Gl5Tfdq7Mc73s0neQshVlSZmhHmZz3MJ-x9mrgGcHagM8FW21eLekYcbeOIKEG-XV7o-EdKq0ADrCwdDOxF3NR0XhNjDoT6kxl3Nh72eYLmoxZEjTjlER6zO1YO4PaW5ZoEAQ9mr8Y91oMN_an9joSHx6RyhGcYVRh7MMvpb2u1uUBvIPO2qCjb8btlh1rbETHBVmp3J9ffYUWGth27SqQxn1g-G3tCgQZUWMxc8g-kLvAmJO2cQ'

presenceUrl = 'https://graph.microsoft.com/beta/users/892d512e-0bd5-455b-94b2-aeb47eb78a07-bogus-user-for-blog/presence'

#presenceUrl = 'https://graph.microsoft.com/v1.0/users/892d512e-0bd5-455b-94b2-aeb47eb78a07-bogus-user-for-blog'

r = requests.get(presenceUrl,headers={'Authorization': 'Bearer {}'.format(accessToken)})

###print(r.headers)

###print(r.status_code)

###print(r.json())

if (r.status_code == 200):

jsonPresenceDict = json.loads(r.text)

presencePrimary = jsonPresenceDict['availability']

presenceSecondary = jsonPresenceDict['activity']

#Presence Info - https://docs.microsoft.com/en-us/microsoftteams/presence-admins

if (presencePrimary == 'Available'):

#Debug code

#LED Tests

###################################################

#RGB - GREEN

###################################################

print("ON - Green")

print("Primary Status: ",presencePrimary)

print("Secondary Status: ",presenceSecondary)

#ON - Green

GPIO.output(blueLedGpio, False)

GPIO.output(redLedGpio, False)

GPIO.output(greenLedGpio, True)

#time.sleep(sleepTimer)

#print("OFF - Green")

#OFF - Green

#GPIO.output(greenLedGpio, False)

#time.sleep(sleepTimer)

###################################################

#Busy

elif (presencePrimary == 'Busy'):

#Debug code

#LED Tests

###################################################

#RGB - RED

###################################################

print("ON - Red")

print("Primary Status: ",presencePrimary)

print("Secondary Status: ",presenceSecondary)

#ON - Red

GPIO.output(greenLedGpio, False)

GPIO.output(blueLedGpio, False)

GPIO.output(redLedGpio, True)

#DND

elif (presencePrimary == 'DoNotDisturb'):

#Debug code

#LED Tests

###################################################

#RGB - RED/BLUE = Magenta

###################################################

print("ON - Magenta")

print("Primary Status: ",presencePrimary)

print("Secondary Status: ",presenceSecondary)

#ON - Red / Blue = Magenta

GPIO.output(greenLedGpio, False)

GPIO.output(blueLedGpio, True)

GPIO.output(redLedGpio, True)

#Away

elif (presencePrimary == 'Away'):

#Debug code

#LED Tests

###################################################

#RGB - Yellow (Actually White as Yellow is to close to Green on the LED

###################################################

print("ON - Yellow (Actually White)")

print("Primary Status: ",presencePrimary)

print("Secondary Status: ",presenceSecondary)

GPIO.output(greenLedGpio, True)

GPIO.output(redLedGpio, True)

GPIO.output(blueLedGpio, True)

elif (presencePrimary == 'BeRightBack'):

#Debug code

#LED Tests

###################################################

#RGB - Blue

###################################################

print("ON - Blue")

print("Primary Status: ",presencePrimary)

print("Secondary Status: ",presenceSecondary)

GPIO.output(greenLedGpio, False)

GPIO.output(redLedGpio, False)

GPIO.output(blueLedGpio, True)

else:

#Debug code

#LED Tests

###################################################

#RGB - Green/Blue = Cyan

###################################################

print("ON - Green/Blue=Cyan")

print("Primary Status: ",presencePrimary)

print("Secondary Status: ",presenceSecondary)

GPIO.output(greenLedGpio, True)

GPIO.output(redLedGpio, False)

GPIO.output(blueLedGpio, True)

except Exception as e:

print("type error: " + str(e))

print(traceback.format_exc())

pass


#######################

#Start of main program#

#######################

if __name__ == "__main__":

#PIR - Detect rising/falling edge on PIR and run callback interrupt

#GPIO.add_event_detect(pir, GPIO.BOTH, callback=triggerAlarm)

try:

#reset counter

counter=0

#get date and time

now = datetime.datetime.now()

print("Starting Busy Light program")


#create the log directory

try:

#Create Directory if it does not exist - sort in days

pathlib.Path('/home/pi/busy_light/logs/').mkdir(parents=True, exist_ok=True)

except:

print("Log directory does not exist and was unable to create or something else went wrong!")

GPIO.cleanup() # this ensures a clean exit

#pass

print(counter,",","Starting Busy Light program",",",now.strftime("%Y-%m-%d %H:%M:%S.%f"),file=open(logPath+"busy_light_"+now.strftime("%Y-%m-%d")+".log", "a"))

#Function to get presence and keep LEDs updated

getMSDeviceFlowAuth()


except KeyboardInterrupt:

# here you put any code you want to run before the program

# exits when you press CTRL+C

GPIO.cleanup() # this ensures a clean exit

except Exception as inst:

print(type(inst)) # the exception instance

print(inst.args) # arguments stored in .args

print(inst) # __str__ allows args to be printed directly,

# but may be overridden in exception subclasses

x, y = inst.args # unpack args

print('x =', x)

print('y =', y)

# this catches ALL other exceptions including errors.

# You won't get any error messages for debugging

# so only use it once your code is working

GPIO.cleanup() # this ensures a clean exit

finally:

GPIO.cleanup() # this ensures a clean exit

Running the script once configured for testing:


#python3 ms_teams_device_flow_presence_api_20200426_1.3.py

Starting Busy Light program

Start to get remote Device Flow Authentication - MS Access Token via Azure Delegated API App.......

{"user_code":"H34X4RZ4A","device_code":"HAQABAAEAAAAm-06blBE1TpVMil8KPQ41TtPEy66rItTAnkKZkkZSQ4FHpXoGWvWG7fil-yVzG35parV3YyHNGf5c16taW-RC8LVhtupONsjglqtJBQQUOA_xe_pa9B1TqF_IHv85kr2wjsmvFaC5UWv8bLPklh2dWXski_ERjMgym4gwEiJZXXRliMjpI5zxUvw-Ni_SoLcgAA","verification_uri":"https://microsoft.com/devicelogin","expires_in":900,"interval":5,"message":"To sign in, use a web browser to open the page https://microsoft.com/devicelogin and enter the code H34X4RZ4A to authenticate."}

To sign in, use a web browser to open the page https://microsoft.com/devicelogin and enter the code H34X4RZ4A to authenticate.

https://microsoft.com/devicelogin

user_code= H34X4RZ4A

device_code= HAQABAAEAAAAm-06blBE1TpVMil8KPQ41TtPEy66rItTAnkKZkkZSQ4FHpXoGWvWG7fil-yVzG35parV3YyHNGf5c16taW-RC8LVhtupONsjglqtJBQQUOA_xe_pa9B1TqF_IHv85kr2wjsmvFaC5UWv8bLPklh2dWXski_ERjMgym4gwEiJZXXRliMjpI5zxUvw-Ni_SoLcgAA

900

5

0

lets get a token......!

{"error":"authorization_pending","error_description":"AADSTS70016: OAuth 2.0 device flow error. Authorization is pending. Continue polling.\r\nTrace ID: ff774e69-881f-4840-b358-0654b7139a00\r\nCorrelation ID: 311a9e69-0289-4ba0-80b1-dd44f556baf9\r\nTimestamp: 2020-04-26 08:19:21Z","error_codes":[70016],"timestamp":"2020-04-26 08:19:21Z","trace_id":"ff774e69-881f-4840-b358-0654b7139a00","correlation_id":"311a9e69-0289-4ba0-80b1-dd44f556baf9","error_uri":"https://login.microsoftonline.com/error?code=70016"}

Still waiting for access token............................................. authorization_pending

1

lets get a token......!

{"error":"authorization_pending","error_description":"AADSTS70016: OAuth 2.0 device flow error. Authorization is pending. Continue polling.\r\nTrace ID: ce85faf5-024c-4bb1-9007-1d852ff94700\r\nCorrelation ID: 311a9e69-0289-4ba0-80b1-dd44f556baf9\r\nTimestamp: 2020-04-26 08:19:27Z","error_codes":[70016],"timestamp":"2020-04-26 08:19:27Z","trace_id":"ce85faf5-024c-4bb1-9007-1d852ff94700","correlation_id":"311a9e69-0289-4ba0-80b1-dd44f556baf9","error_uri":"https://login.microsoftonline.com/error?code=70016"}

Still waiting for access token............................................. authorization_pending

lets get a token......!

{"token_type":"Bearer","scope":"Presence.Read Presence.Read.All SecurityActions.Read.All SecurityActions.ReadWrite.All SecurityEvents.Read.All SecurityEvents.ReadWrite.All ThreatIndicators.Read.All ThreatIndicators.ReadWrite.OwnedBy User.Read User.Read.All profile openid emai


...

...........


Start MS Teams Presence gather every 5 seconds..........Checked 45 times

ON - Green

Primary Status: Available

Secondary Status: Available

Start MS Teams Presence gather every 5 seconds..........Checked 46 times

ON - Green

Primary Status: Available

Secondary Status: Available

Start MS Teams Presence gather every 5 seconds..........Checked 47 times

ON - Red

Primary Status: Busy

Secondary Status: Busy

Start MS Teams Presence gather every 5 seconds..........Checked 48 times

ON - Magenta

Primary Status: DoNotDisturb

Secondary Status: DoNotDisturb

Start MS Teams Presence gather every 5 seconds..........Checked 49 times

ON - Magenta

Primary Status: DoNotDisturb

Secondary Status: DoNotDisturb

Start MS Teams Presence gather every 5 seconds..........Checked 50 times

ON - Magenta

Primary Status: DoNotDisturb

Secondary Status: DoNotDisturb

Start MS Teams Presence gather every 5 seconds..........Checked 51 times

ON - Blue

Primary Status: BeRightBack

Secondary Status: BeRightBack

Start MS Teams Presence gather every 5 seconds..........Checked 52 times

ON - Blue

Primary Status: BeRightBack

Secondary Status: BeRightBack


========================

Presence indication e.g. Green and Red below:

Green


Red