If you are using Team Meeting Room devices, in this case for Android as the devices are Logitech devices then you need to ensure that the team client is installed is updated after the firmware has been updated on the Logitech device.
The new firmware on the Logitech device enabled the new features in the teams application so it makes sense to update the hardware before the software on the hardware running teams, this information can be extracted from Team Admin Centre (TAC) however sometimes a script can more effectively query this data and present a report.Note: You must remember with API connection you get 20 results by default so you need to use pagination to return more than the first page of results, this script handles that.
Pre-Flight : Create the App Registration
First we need to create the App Registration so we need to head over to Entra:
Then we need App Registrations
Then we need App Registrations
Then we need a new registration as below:
From here we need Certificates and Secrets as below:
Then ensure you have "client secrets" selected and choose "New client secret"
You then need to give that secret a name and lifetime:
You will then see this secret below:
Note : You will only see the value once when you navigate away from this screen and back the value will no longer be visible!
Next we need API permissions:
From here we need Certificates and Secrets as below:
Then ensure you have "client secrets" selected and choose "New client secret"
You then need to give that secret a name and lifetime:
You will then see this secret below:
Note : You will only see the value once when you navigate away from this screen and back the value will no longer be visible!
Next we need API permissions:
Then you need an API permission, for this we need Microsoft Graph:
Then you need an application permission:
Now we need to find the permissions so when you get the search option enter "TeamWork" here then choose the permissions TeamworkDevice.Read.All and TeamworkDevice.ReadWrite.All as below:
Then you need an application permission:
Now we need to find the permissions so when you get the search option enter "TeamWork" here then choose the permissions TeamworkDevice.Read.All and TeamworkDevice.ReadWrite.All as below:
When you add these permission you should see then as valid API permissions as below:
You then need to grant admin consent for these permissions:
Which you will need to confirm:
Then you will see that the "granted concent" is now approved with the green ticks:
You will also need to know the tenant ID and application ID which you can get from the overview section of the App Registration:
You will need the following information for this script to work so keep it handy:
Tenant ID
You then need to grant admin consent for these permissions:
Which you will need to confirm:
Then you will see that the "granted concent" is now approved with the green ticks:
You will also need to know the tenant ID and application ID which you can get from the overview section of the App Registration:
You will need the following information for this script to work so keep it handy:
Tenant ID
Application ID
Secret Key
Pre-Flight : Install Team Modules
This can be completed with a very simple script as below:
Script : Install-Required.ps1
Script : Install-Required.ps1
Write-Host "Starting module installation and import process..." -ForegroundColor Cyan
Write-Host "Installing required modules..." -ForegroundColor Yellow
Install-Module -Name MicrosoftTeams -Force -AllowClobber
Install-Module -Name MSAL.PS -Force -AllowClobber -SkipPublisherCheck
Write-Host "Importing modules..." -ForegroundColor Yellow
Import-Module MicrosoftTeams
Import-Module MSAL.PS
Write-Host "Module setup complete." -ForegroundColor Green
Mission Control : Run the Main Script for data extraction
Now we need to run the main script to get this data from Teams using the API and Graph, you should update the variables for this to work (the data you got from ealier)
Script : PatchStatusChecker.ps1
# Authentication Variables
$TenantId = "<tenant_ID>"
$ClientId = "<application_id>"
$ClientSecret = "<client_secret>"
# Skip WebView2 dependency
$env:SKIP_MSAL_PS_WEBVIEW2_CHECK = $true
Write-Host "Starting module installation and import process..." -ForegroundColor Cyan
Import-Module MicrosoftTeams
Import-Module MSAL.PS
Write-Host "Module setup complete." -ForegroundColor Green
# Define API endpoints and scopes
$apiUrl = "https://graph.microsoft.com/beta/teamwork/devices"
$scopes = "https://graph.microsoft.com/.default"
function Get-AccessToken {
param (
[string]$TenantId,
[string]$ClientId,
[string]$ClientSecret
)
Write-Host "Attempting to acquire access token..." -ForegroundColor Yellow
$authority = "https://login.microsoftonline.com/$TenantId/oauth2/v2.0/token"
$tokenBody = @{
grant_type = 'client_credentials'
client_id = $ClientId
client_secret = $ClientSecret
scope = $scopes
}
try {
Write-Host "Sending token request with scope: $scopes" -ForegroundColor Gray
$response = Invoke-RestMethod -Uri $authority -Method Post -Body $tokenBody
Write-Host "Successfully acquired access token." -ForegroundColor Green
return $response.access_token
}
catch {
Write-Error "Failed to get access token: $_"
Write-Host "Authentication failed. Error details:" -ForegroundColor Red
Write-Host $_.Exception.Message -ForegroundColor Red
exit
}
}
# Get access token
Write-Host "`nInitiating authentication process..." -ForegroundColor Cyan
$accessToken = Get-AccessToken -TenantId $TenantId -ClientId $ClientId -ClientSecret $ClientSecret
# Create header for REST calls
Write-Host "Setting up API headers with access token..." -ForegroundColor Yellow
$headers = @{
'Authorization' = "Bearer $accessToken"
'Content-Type' = 'application/json'
}
# Get all Teams devices with pagination
$results = @()
$nextLink = $apiUrl
try {
do {
Write-Host "`nQuerying Teams API for device list..." -ForegroundColor Cyan
$response = Invoke-RestMethod -Uri $nextLink -Headers $headers -Method Get
$devices = $response.value
Write-Host "Retrieved batch of $($devices.Count) devices" -ForegroundColor Green
Write-Host "Processing batch of devices..." -ForegroundColor Yellow
foreach ($device in $devices) {
try {
$deviceDetailUrl = "$apiUrl/$($device.id)"
Write-Host "Retrieving details for device ID: $($device.id)" -ForegroundColor Gray
$deviceInfo = Invoke-RestMethod -Uri $deviceDetailUrl -Headers $headers -Method Get
# Get display name from currentUser object
$roomName = if ($deviceInfo.currentUser -and $deviceInfo.currentUser.displayName) {
$deviceInfo.currentUser.displayName
} else {
"Unknown Room"
}
$deviceStatus = [PSCustomObject]@{
'RoomName' = $roomName
'DeviceId' = $deviceInfo.id
'HealthStatus' = $deviceInfo.healthStatus
'LastSeen' = $deviceInfo.lastSignInDateTime
'Model' = $deviceInfo.hardwareDetail.model
'Manufacturer' = $deviceInfo.hardwareDetail.manufacturer
'SerialNumber' = $deviceInfo.hardwareDetail.serialNumber
'OSVersion' = $deviceInfo.operatingSystem.version
}
$results += $deviceStatus
Write-Host "Successfully processed device: $roomName" -ForegroundColor Green
}
catch {
Write-Warning "Failed to process device $($device.id): $_"
continue
}
}
$nextLink = $response.'@odata.nextLink'
Write-Host "Total devices processed so far: $($results.Count)" -ForegroundColor Cyan
} while ($nextLink)
}
catch {
Write-Error "Failed to retrieve devices: $_"
Write-Host "Device retrieval failed. Exiting script." -ForegroundColor Red
Write-Host "Error details: $($_.Exception.Message)" -ForegroundColor Red
exit
}
# Export results to CSV
$csvPath = "TeamsDeviceStatus_$(Get-Date -Format 'yyyyMMdd').csv"
Write-Host "`nExporting results to CSV..." -ForegroundColor Cyan
try {
$results | Export-Csv -Path $csvPath -NoTypeInformation
Write-Host "Successfully exported results to $csvPath" -ForegroundColor Green
}
catch {
Write-Error "Failed to export CSV: $_"
Write-Host "CSV export failed." -ForegroundColor Red
}
# Display summary
Write-Host "`nFinal Device Status Summary:" -ForegroundColor Cyan
Write-Host "Total devices processed: $($results.Count)" -ForegroundColor White
$results | Format-Table -AutoSize
Write-Host "`nScript execution completed." -ForegroundColor Green
When you run the script it will connect to the Teams API and use the API permissions to get a list of all the Team Devices from the API calls as you can see below:
This will then return all the devices in blocks of 20 (which is 20 per page) until all the pages are complete:
When completed you will end up with a CSV file as below:
This will contain the data of all your Team devices with the CSV headers which include the following (the OSVersion only applies to Windows devices)
"RoomName","DeviceId","HealthStatus","LastSeen","Model","Manufacturer","SerialNumber","OSVersion
In this report the HealthStatus is what we are interested in, and more specifically if thatvalue that is set to "nonUrgent" then that signifies an update is required (these should be Healthy if all is up to date) and if the status is "NonUgent" we need a notification about that.This will contain the data of all your Team devices with the CSV headers which include the following (the OSVersion only applies to Windows devices)
"RoomName","DeviceId","HealthStatus","LastSeen","Model","Manufacturer","SerialNumber","OSVersion
email Notifications
If you wish to get an email notification with that report attached then you can use this code to accomplish that, this will send a e-mail only if more than 20 devices are in the status nonUrgent the you can use this script, however you will need to design your own html file that will be used in the email, modify the parameters in bold.
Script : smtp-mailer.ps1
# Function to write verbose output
function Write-Log {
param($Message)
Write-Host "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss'): $Message"
}
Write-Log "Script started - Searching for most recent CSV file"
# Get the most recent CSV file in current directory
$csvFile = Get-ChildItem -Filter "*.csv" | Sort-Object LastWriteTime -Descending | Select-Object -First 1
Write-Log "Found CSV file: $($csvFile.Name)"
# Read and analyze the CSV
Write-Log "Reading CSV file..."
$devices = Import-Csv $csvFile
Write-Log "Total devices found in CSV: $($devices.Count)"
$noUrgentCount = ($devices | Where-Object { $_.HealthStatus -eq "nonUrgent" } | Measure-Object).Count
Write-Log "Devices with 'nonUrgent' status: $noUrgentCount"
if ($noUrgentCount -gt 20) {
Write-Log "noUrgent count exceeds threshold (20). Preparing email..."
# Read HTML template
Write-Log "Reading HTML template from Teams.html"
$htmlBody = Get-Content ".\Teams.html" -Raw
Write-Log "HTML template loaded successfully"
# Configure email
Write-Log "Configuring email parameters..."
$emailParams = @{
From = "teams.alert@croucher.cloud"
To = "lee@croucher.cloud"
Subject = "Teams : Meeting Room Patch Status"
Body = $htmlBody
BodyAsHtml = $true
SmtpServer = "smtp1.bear.local"
Attachments = $csvFile.FullName
}
# Send email
Write-Log "Attempting to send email..."
try {
Send-MailMessage @emailParams
Write-Log "Email sent successfully"
}
catch {
Write-Log "ERROR: Failed to send email: $($_.Exception.Message)"
}
}
else {
Write-Log "noUrgent count ($noUrgentCount) does not exceed threshold. No email sent."
}
Write-Log "Script completed"
I have chosen to design the HTML template to be modern and minimalistic as below:
Looking to update without reporting?
If you are looking to update the Team app on these devices and you do not want a "out of date" report then you can choose to use the script below which will start the update on all the devices in TAC in a gradual rollout so that the API is not overwhelmed.
Looking to update without reporting?
If you are looking to update the Team app on these devices and you do not want a "out of date" report then you can choose to use the script below which will start the update on all the devices in TAC in a gradual rollout so that the API is not overwhelmed.
Note : Ensure the manufacturer firmware is up to date, in this case the Logitech firmware will need to be up to date via the Logitech Sync Portal.
If you are happy you wish to proceed then you can use the script below, however you do so at your own risk and the author of this blog cannot be held responsable for issues you may encounter as each environment is different, I have added throttling variables for a slow controlled rollout.
Script : TeamRoom-Updater.ps1
# Authentication Variables
$TenantId = "<tenant_id>"
$ClientId = "<client_id>"
$ClientSecret = "<secret>"
# Throttling Configuration
$ThrottleLimit = 5 # Number of concurrent requests
$DelayBetweenRequests = 2 # Seconds between requests
$BatchSize = 10 # Number of devices to process before a longer pause
$BatchPauseSeconds = 30 # Seconds to pause between batches
# Skip WebView2 dependency
$env:SKIP_MSAL_PS_WEBVIEW2_CHECK = $true
Write-Host "`n=====================================" -ForegroundColor Cyan
Write-Host "Teams Room System Update Script" -ForegroundColor Cyan
Write-Host "=====================================`n" -ForegroundColor Cyan
Write-Host "Throttling Configuration:" -ForegroundColor Yellow
Write-Host "- Concurrent requests: $ThrottleLimit" -ForegroundColor Gray
Write-Host "- Delay between requests: $DelayBetweenRequests seconds" -ForegroundColor Gray
Write-Host "- Batch size: $BatchSize devices" -ForegroundColor Gray
Write-Host "- Batch pause: $BatchPauseSeconds seconds" -ForegroundColor Gray
Write-Host "`nStarting module installation and import process..." -ForegroundColor Cyan
Import-Module MicrosoftTeams
Import-Module MSAL.PS
Write-Host "Module setup complete." -ForegroundColor Green
# Define API endpoints and scopes
$apiUrl = "https://graph.microsoft.com/beta/teamwork/devices"
$scopes = "https://graph.microsoft.com/.default"
function Get-AccessToken {
param (
[string]$TenantId,
[string]$ClientId,
[string]$ClientSecret
)
Write-Host "`nAttempting to acquire access token..." -ForegroundColor Yellow
$authority = "https://login.microsoftonline.com/$TenantId/oauth2/v2.0/token"
$tokenBody = @{
grant_type = 'client_credentials'
client_id = $ClientId
client_secret = $ClientSecret
scope = $scopes
}
try {
Write-Host "Sending token request..." -ForegroundColor Gray
$response = Invoke-RestMethod -Uri $authority -Method Post -Body $tokenBody
Write-Host "Successfully acquired access token." -ForegroundColor Green
return $response.access_token
}
catch {
Write-Error "Failed to get access token: $_"
Write-Host "Authentication failed. Error details:" -ForegroundColor Red
Write-Host $_.Exception.Message -ForegroundColor Red
exit
}
}
function Start-ThrottledDeviceUpdate {
param (
[string]$DeviceId,
[string]$RoomName,
[hashtable]$Headers,
[int]$RetryCount = 3,
[int]$RetryDelay = 5
)
$updateUrl = "$apiUrl/$DeviceId/updateSettings"
$updateBody = @{
"allowUpdateTime" = "Always"
"updateMode" = "Force"
} | ConvertTo-Json
for ($i = 0; $i -lt $RetryCount; $i++) {
try {
if ($i -gt 0) {
Write-Host "Retry attempt $($i + 1) for $RoomName..." -ForegroundColor Yellow
}
Write-Host "`nInitiating force update for room: $RoomName" -ForegroundColor Yellow
Write-Host "Device ID: $DeviceId" -ForegroundColor Gray
$response = Invoke-RestMethod -Uri $updateUrl -Headers $Headers -Method Patch -Body $updateBody -ContentType "application/json"
Write-Host "Update command sent successfully" -ForegroundColor Green
return $true
}
catch {
if ($_.Exception.Response.StatusCode -eq 429) {
# Handle rate limiting
$retryAfter = 30
if ($_.Exception.Response.Headers["Retry-After"]) {
$retryAfter = [int]$_.Exception.Response.Headers["Retry-After"]
}
Write-Host "Rate limit hit, waiting $retryAfter seconds..." -ForegroundColor Yellow
Start-Sleep -Seconds $retryAfter
continue
}
elseif ($i -lt $RetryCount - 1) {
Write-Host "Failed attempt $($i + 1). Retrying in $RetryDelay seconds..." -ForegroundColor Yellow
Start-Sleep -Seconds $RetryDelay
continue
}
else {
Write-Host "Failed to initiate update for $RoomName after $RetryCount attempts" -ForegroundColor Red
Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Red
return $false
}
}
}
}
# Get access token
Write-Host "`nInitiating authentication process..." -ForegroundColor Cyan
$accessToken = Get-AccessToken -TenantId $TenantId -ClientId $ClientId -ClientSecret $ClientSecret
# Create header for REST calls
$headers = @{
'Authorization' = "Bearer $accessToken"
'Content-Type' = 'application/json'
}
# Initialize results arrays
$successUpdates = @()
$failedUpdates = @()
# Get all Teams devices
Write-Host "`nRetrieving Teams Rooms devices..." -ForegroundColor Cyan
$nextLink = $apiUrl
$devices = @()
try {
do {
$response = Invoke-RestMethod -Uri $nextLink -Headers $headers -Method Get
$devices += $response.value
$nextLink = $response.'@odata.nextLink'
Start-Sleep -Seconds 1 # Throttle device list retrieval
} while ($nextLink)
$totalDevices = $devices.Count
Write-Host "Found $totalDevices devices to process" -ForegroundColor Green
# Process devices in batches
for ($i = 0; $i -lt $devices.Count; $i++) {
$device = $devices[$i]
$deviceDetailUrl = "$apiUrl/$($device.id)"
# Add delay between device detail requests
if ($i -gt 0) {
Start-Sleep -Seconds $DelayBetweenRequests
}
# Pause between batches
if ($i -gt 0 -and $i % $BatchSize -eq 0) {
Write-Host "`nProcessed $i of $totalDevices devices. Pausing for $BatchPauseSeconds seconds..." -ForegroundColor Yellow
Start-Sleep -Seconds $BatchPauseSeconds
}
try {
$deviceInfo = Invoke-RestMethod -Uri $deviceDetailUrl -Headers $headers -Method Get
$roomName = if ($deviceInfo.currentUser -and $deviceInfo.currentUser.displayName) {
$deviceInfo.currentUser.displayName
} else {
"Unknown Room"
}
Write-Host "`n----------------------------------------" -ForegroundColor Cyan
Write-Host "Processing Device $($i + 1) of $totalDevices" -ForegroundColor Cyan
Write-Host "Room: $roomName" -ForegroundColor Cyan
Write-Host "Model: $($deviceInfo.hardwareDetail.model)" -ForegroundColor Gray
Write-Host "Serial Number: $($deviceInfo.hardwareDetail.serialNumber)" -ForegroundColor Gray
$updateResult = Start-ThrottledDeviceUpdate -DeviceId $device.id -RoomName $roomName -Headers $headers
$updateStatus = [PSCustomObject]@{
'RoomName' = $roomName
'DeviceId' = $device.id
'Model' = $deviceInfo.hardwareDetail.model
'SerialNumber' = $deviceInfo.hardwareDetail.serialNumber
'UpdateInitiated' = Get-Date
'Status' = if ($updateResult) { "Update Initiated" } else { "Failed to Initiate" }
}
if ($updateResult) {
$successUpdates += $updateStatus
} else {
$failedUpdates += $updateStatus
}
}
catch {
Write-Warning "Failed to process device $($device.id): $_"
continue
}
}
# Generate summary report
$csvPath = "TeamsRoomUpdates_$(Get-Date -Format 'yyyyMMdd_HHmmss').csv"
Write-Host "`n=============================================" -ForegroundColor Cyan
Write-Host "Update Process Summary" -ForegroundColor Cyan
Write-Host "=============================================" -ForegroundColor Cyan
Write-Host "Total Devices Processed: $($devices.Count)" -ForegroundColor White
Write-Host "Successful Updates Initiated: $($successUpdates.Count)" -ForegroundColor Green
Write-Host "Failed Updates: $($failedUpdates.Count)" -ForegroundColor Red
# Export results to CSV
$allUpdates = $successUpdates + $failedUpdates
$allUpdates | Export-Csv -Path $csvPath -NoTypeInformation
Write-Host "`nDetailed results exported to: $csvPath" -ForegroundColor Yellow
# Display results in console
Write-Host "`nSuccessful Updates:" -ForegroundColor Green
$successUpdates | Format-Table -AutoSize
if ($failedUpdates.Count -gt 0) {
Write-Host "`nFailed Updates:" -ForegroundColor Red
$failedUpdates | Format-Table -AutoSize
}
}
catch {
Write-Error "Script execution failed: $_"
Write-Host "Error details: $($_.Exception.Message)" -ForegroundColor Red
}
Write-Host "`nScript execution completed." -ForegroundColor Green