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
Presence info for Teams: https://docs.microsoft.com/en-us/microsoftteams/presence-admins
Why would you use device code flow: https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/wiki/Device-Code-Flow
Great overview of Oauth2 when it comes to understanding device code flow: https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-device-code
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: