Powershell : Teams for Room Meeting Room Device Updates - via API/Graph

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 a new registration as below:




Then we need to give it a valid name and ensure its single tenant then Register this application:



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:

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
Application ID
Secret Key

Pre-Flight : Install Team Modules

This can be completed with a very simple script as below:

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.

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.

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 
Previous Post Next Post

نموذج الاتصال