Public/UserManagement/VoiceConfig/Get-TeamsUserVoiceConfig.ps1

# Module: TeamsFunctions
# Function: VoiceConfig
# Author: David Eberhardt
# Updated: 01-DEC-2020
# Status: Live




function Get-TeamsUserVoiceConfig {
  <#
  .SYNOPSIS
    Displays Voice Configuration Parameters for one or more Users
  .DESCRIPTION
    Displays Voice Configuration Parameters with different Diagnostic Levels
    ranging from basic Voice Configuration up to Policies, Account Status & DirSync Information
  .PARAMETER UserPrincipalName
    Required. UserPrincipalName (UPN) of the User
  .PARAMETER DiagnosticLevel
    Optional. Value from 0 to 4. Higher values will display more parameters
    If not provided (and not suppressed with SkipLicenseCheck), will change the output of LicensesAssigned to ProductNames only
    See NOTES below for details.
  .PARAMETER SkipLicenseCheck
    Optional. Will not perform queries against User Licensing to improve performance
  .PARAMETER ReturnObjectIfNotFound
    Optional. Returns an empty object for when no CsOnlineUser Object nor AzureAdUser Object can be found.
    This is useful for bulk-operations exporting this information to CSV
  .PARAMETER WriteErrorLog
    Optional. If Errors are encountered, writes log to C:\Temp
  .EXAMPLE
    Get-TeamsUserVoiceConfig -UserPrincipalName John@domain.com
 
    Shows Voice Configuration for John with a concise view of Parameters
  .EXAMPLE
    Get-TeamsUserVoiceConfig -UserPrincipalName John@domain.com -DiagnosticLevel 2
 
    Shows Voice Configuration for John with a extended list of Parameters (see NOTES)
  .EXAMPLE
    "John@domain.com" | Get-TeamsUserVoiceConfig -SkipLicenseCheck
 
    Shows Voice Configuration for John with a concise view of Parameters and skips validation of Licensing for this User.
  .EXAMPLE
    Get-CsOnlineUser | Where-Object UsageLocation -eq "BE" | Get-TeamsUserVoiceConfig
 
    Shows Voice Configuration for all CsOnlineUsers with a UsageLocation set to Belgium. Returns concise view of Parameters
    For best results, please filter the Users first and add Diagnostic Levels at your discretion
  .EXAMPLE
    Get-TeamsUserVoiceConfig "NonExistentUser@domain.com" -WriteErrorLog -ReturnObjectIfNotFound -DiagnosticLevel 3 | Export-Csv "C:\Temp\Get-TeamsUVC.csv" -Append
 
    Assuming the user does not exist, will write an error log to C:\Temp and returns an empty object with Diagnostic Level 3
    The Output is written to a CSV file containing all parsed objects, whether found or not.
  .INPUTS
    System.String
  .OUTPUTS
    System.Object
  .NOTES
    DiagnosticLevel details:
    1 Basic diagnostics for Hybrid Configuration or when moving users from On-prem Skype
    2 Extended diagnostics displaying additional Voice-related Policies
    3 Basic troubleshooting parameters from AzureAD like AccountEnabled, etc.
    4 Extended troubleshooting parameters from AzureAD like LastDirSyncTime
    Parameters are additive, meaning with each DiagnosticLevel more information is displayed
 
    This script takes a select set of Parameters from AzureAD, Teams & Licensing. For a full parameterset, please run:
    - for AzureAD: "Find-AzureAdUser $UserPrincipalName | FL"
    - for Licensing: "Get-AzureAdUserLicense $UserPrincipalName"
    - for Teams: "Get-CsOnlineUser $UserPrincipalName"
 
    Exporting PowerShell Objects that contain Nested Objects as CSV results in this parameter being shown as "System.Object[]".
    Using any diagnostic level higher than 3 adds Parameter LicenseObject allowing to drill-down into Licensing
    Omitting it allows for visible data when exporting as a CSV.
  .COMPONENT
    VoiceConfiguration
  .FUNCTIONALITY
    Returns an Object to validate the Voice Configuration for an Object
  .LINK
    https://github.com/DEberhardt/TeamsFunctions/tree/main/docs/Get-TeamsUserVoiceConfig.md
  .LINK
    https://github.com/DEberhardt/TeamsFunctions/tree/main/docs/about_VoiceConfiguration.md
  .LINK
    https://github.com/DEberhardt/TeamsFunctions/tree/main/docs/
  #>


  [CmdletBinding()]
  [Alias('Get-TeamsUVC')]
  [OutputType([PSCustomObject])]
  param(
    [Parameter(Mandatory, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName)]
    [Alias('ObjectId', 'Identity')]
    [string[]]$UserPrincipalName,

    [Parameter(HelpMessage = 'Defines level of Diagnostic Data that are added to the output object')]
    [Alias('DiagLevel', 'Level', 'DL')]
    [ValidateRange(0, 4)]
    [int32]$DiagnosticLevel,

    [Parameter(HelpMessage = 'Improves performance by not performing a License Check on the User')]
    [Alias('SkipLicense', 'SkipLic')]
    [switch]$SkipLicenseCheck,

    [Parameter(HelpMessage = 'Returns an empty Object instead of terminating if no Object has been found')]
    [Alias('ReturnEmptyObject')]
    [switch]$ReturnObjectIfNotFound,

    [Parameter(HelpMessage = 'Writes errors to C:\Temp')]
    [switch]$WriteErrorLog

  ) #param

  begin {
    Show-FunctionStatus -Level Live
    Write-Verbose -Message "[BEGIN ] $($MyInvocation.MyCommand)"

    # Asserting AzureAD Connection
    if ( -not $script:TFPSSA) { $script:TFPSSA = Assert-AzureADConnection; if ( -not $script:TFPSSA ) { break } }

    # Asserting MicrosoftTeams Connection
    if ( -not (Assert-MicrosoftTeamsConnection) ) { break }

    # Setting Preference Variables according to Upstream settings
    if (-not $PSBoundParameters.ContainsKey('Verbose')) { $VerbosePreference = $PSCmdlet.SessionState.PSVariable.GetValue('VerbosePreference') }
    if (-not $PSBoundParameters.ContainsKey('Confirm')) { $ConfirmPreference = $PSCmdlet.SessionState.PSVariable.GetValue('ConfirmPreference') }
    if (-not $PSBoundParameters.ContainsKey('WhatIf')) { $WhatIfPreference = $PSCmdlet.SessionState.PSVariable.GetValue('WhatIfPreference') }
    if (-not $PSBoundParameters.ContainsKey('Debug')) { $DebugPreference = $PSCmdlet.SessionState.PSVariable.GetValue('DebugPreference') } else { $DebugPreference = 'Continue' }
    if ( $PSBoundParameters.ContainsKey('InformationAction')) { $InformationPreference = $PSCmdlet.SessionState.PSVariable.GetValue('InformationAction') } else { $InformationPreference = 'Continue' }

    #Initialising Counters
    $private:StepsID0, $private:StepsID1 = Get-WriteBetterProgressSteps -Code $($MyInvocation.MyCommand.Definition) -MaxId 1
    $private:ActivityID0 = $($MyInvocation.MyCommand.Name)
    [int] $private:CountID0 = [int] $private:CountID1 = 1

    # Adding Types
    Add-Type -AssemblyName Microsoft.Open.AzureAD16.Graph.Client
    Add-Type -AssemblyName Microsoft.Open.Azure.AD.CommonLibrary

    # preparing Output Field Separator
    $OFS = ', ' # do not remove - Automatic variable, used to separate elements!

    # Querying Teams Module Version
    #if ( -not $global:TeamsFunctionsMSTeamsModule) { $global:TeamsFunctionsMSTeamsModule = Get-Module MicrosoftTeams }

  } #begin

  process {
    Write-Verbose -Message "[PROCESS] $($MyInvocation.MyCommand)"
    foreach ($User in $UserPrincipalName) {
      [int] $private:CountID0 = 1
      #region Information Gathering
      $StatusID0 = "Processing '$User' - Information Gathering"
      #region Querying Identity
      $CsUser = $AdUser = $null
      try {
        $CurrentOperationID0 = 'Querying User Account (CsOnlineUser)'
        Write-BetterProgress -Id 0 -Activity $ActivityID0 -Status $StatusID0 -CurrentOperation $CurrentOperationID0 -Step ($private:CountID0++) -Of $private:StepsID0
        $CsUser = Get-CsOnlineUser -Identity "$User" -WarningAction SilentlyContinue -ErrorAction Stop
        $ObjectUPN = $($CsUser.UserPrincipalName)
      }
      catch {
        # If CsOnlineUser not found, trying AzureAdUser
        try {
          $CurrentOperationID0 = 'Querying User Account (AzureAdUser)'
          Write-BetterProgress -Id 0 -Activity $ActivityID0 -Status $StatusID0 -CurrentOperation $CurrentOperationID0 -Step ($private:CountID0++) -Of $private:StepsID0
          #New Graph Equivalent to be rolled out through Refactor in Orbit
          #$AdUser = Get-MgUser -UserId "$User" -WarningAction SilentlyContinue -ErrorAction STOP
          $AdUser = Get-AzureADUser -ObjectId "$User" -WarningAction SilentlyContinue -ErrorAction STOP
          $CsUser = $AdUser
          $ObjectUPN = $($AdUser.UserPrincipalName)
          $WarnMessage = "'$User' found in AzureAd but not in Teams (CsOnlineUser)!"
          Write-Warning -Message $WarnMessage
          if ( $WriteErrorLog.IsPresent ) { Write-TFErrorLog -ErrorLog $ErrorMessage -Artifact "$($AdUser.UserPrincipalName)" }
          Write-Verbose -Message 'You may receive this message if no License containing Teams is assigned or the Teams ServicePlan (TEAMS1) is disabled! Please validate the User License. No further validation is performed. The Object returned only contains data from AzureAd' -Verbose
        }
        catch [Microsoft.Open.AzureAD16.Client.ApiException] {
          $ErrorMessage = "'$User' does not exist or one of its queried reference-property objects are not present."
          Write-Error -Message "'$User' not found in Teams (CsOnlineUser) nor in Azure Ad (AzureAdUser). Please validate UserPrincipalName. $ErrorMessage" -Category ObjectNotFound
          if ( $WriteErrorLog.IsPresent ) { Write-TFErrorLog -ErrorLog $ErrorMessage -Artifact "$User" }
          if ( $ReturnObjectIfNotFound.IsPresent ) { $ObjectUPN = $User } else { continue }
        }
        catch {
          $ErrorMessage = $($_.Exception.Message)
          Write-Error -Message "'$User' not found. Error encountered: $($_.Exception.Message)" -Category ObjectNotFound
          if ( $WriteErrorLog ) { Write-TFErrorLog -ErrorLog $ErrorMessage -Artifact "$User" }
          if ( $ReturnObjectIfNotFound.IsPresent ) { $ObjectUPN = $User } else { continue }
        }
      }
      #endregion

      $StatusID0 = "Processing '$ObjectUPN' - Verification"
      #region Constructing VoiceConfigurationType, ObjectType & Misconfiguration
      $CurrentOperationID0 = 'Parsing NumberType'
      Write-BetterProgress -Id 0 -Activity $ActivityID0 -Status $StatusID0 -CurrentOperation $CurrentOperationID0 -Step ($private:CountID0++) -Of $private:StepsID0
      if ( $CsUser.LineUri ) {
        try {
          $TeamsPhoneNumberObject = Get-CsPhoneNumberAssignment -TelephoneNumber $($CsUser.LineUri | Format-StringForUse -As Number) -ErrorAction Stop
          $VoiceConfigurationType = switch ($TeamsPhoneNumberObject.PstnPartnerName) {
            'Microsoft' { 'CallingPlan' }
            '' { 'DirectRouting' }
            Default { 'OperatorConnect' }
          }
        }
        catch {
          Write-Verbose -Message 'Phone Number Query: No phone number found with Get-CsPhoneNumberAssignment'
        }
      }
      else {
        $VoiceConfigurationType = 'DirectRouting'
      }

      $CurrentOperationID0 = 'Parsing ObjectType'
      Write-BetterProgress -Id 0 -Activity $ActivityID0 -Status $StatusID0 -CurrentOperation $CurrentOperationID0 -Step ($private:CountID0++) -Of $private:StepsID0
      try {
        $ObjectType = if ($CsUser.AccountType) { $CsUser.AccountType }
        else {
          if ( $CsUser.InterpretedUserType -match 'User' ) { 'User' }
          elseif ( $CsUser.InterpretedUserType -match 'ApplicationInstance' ) { 'ResourceAccount' }
          else {
            Write-Verbose -Message 'ObjectType cannot be parsed through AccountType or InterpretedUserType - Querying Get-TeamsObjectType'
            Get-TeamsObjectType -Identity "$($CsUser.UserPrincipalName)" -ErrorAction STOP
          }
        }
      }
      catch {
        $ObjectType = 'Unknown'
      }

      $CurrentOperationID0 = 'Testing for Misconfiguration (Test-TeamsUserVoiceConfig)'
      Write-BetterProgress -Id 0 -Activity $ActivityID0 -Status $StatusID0 -CurrentOperation $CurrentOperationID0 -Step ($private:CountID0++) -Of $private:StepsID0
      if ( $AdUser -ne $CsUser ) {
        # Necessary as Test-TeamsUserVoiceConfig expects a CsOnlineUser Object
        $null = Test-TeamsUserVoiceConfig -Object $CsUser -ErrorAction SilentlyContinue
      }
      else {
        Write-Verbose -Message 'No validation can be performed for the Object as CsOnlineUser Object not found!'
      }

      #Info about unassigned Dial Plan (suppressing feedback if AzureAdUser is already populated)
      if ( $CsUser.SipAddress -and $ObjectType -ne 'ResourceAccount' ) {
        if ( -not $CsUser.TenantDialPlan ) {
          Write-Information "INFO: User '$ObjectUPN' - No Dial Plan is assigned"
        }
      }
      #endregion
      #endregion

      #region Creating Base Custom Object
      $CurrentOperationID0 = 'Preparing Output Object'
      Write-BetterProgress -Id 0 -Activity $ActivityID0 -Status $StatusID0 -CurrentOperation $CurrentOperationID0 -Step ($private:CountID0++) -Of $private:StepsID0
      # Adding Basic parameters
      $UserObject = $null
      $UserObject = [PSCustomObject][ordered]@{
        PSTypeName                = 'PowerShell.TeamsFunctsions.UserVoiceConfig'
        UserPrincipalName         = $ObjectUPN
        SipAddress                = $CsUser.SipAddress
        DisplayName               = $CsUser.DisplayName
        Identity                  = $CsUser.Identity
        ObjectType                = $ObjectType
        InterpretedUserType       = $CsUser.InterpretedUserType
        UserValidationErrors      = $CsUser.UserValidationErrors
        VoiceConfigurationType    = $VoiceConfigurationType
        UsageLocation             = $CsUser.UsageLocation
        FeatureTypes              = [string]$CsUser.FeatureTypes
      }

      # Adding Licensing Parameters if not skipped
      if ( $PSBoundParameters.ContainsKey('SkipLicenseCheck') ) {
        Write-Information "INFO: User '$ObjectUPN' - License Query skipped"
      }
      else {
        try {
          Write-Verbose "User '$ObjectUPN' - Querying License details"
          $UserLicenseDetail = Get-AzureADUserLicenseDetail -ObjectId "$($CsUser.UserPrincipalName)" -ErrorAction STOP
          if ( $UserLicenseDetail ) {
            $UserLicenses = Get-UserLicensesFromLicenseDetailObject -UserLicenseDetail $UserLicenseDetail @args
          }
        }
        catch {
          Write-Verbose -Message "'$($CsUser.UserPrincipalName)' License Query: No licenses assigned to this account"
          $UserLicenseDetail = $null
          $UserLicenses = $null
        }
        $UserObject | Add-Member -MemberType NoteProperty -Name LicensesAssigned -Value $($UserLicenses.ProductName -join ', ')

        # Adding additional LicenseObject Parameter with nested Object if Diagnostic Level is high enough (3 or higher)
        if ( $DiagnosticLevel -ge 3 ) {
          $UserObject | Add-Member -MemberType NoteProperty -Name LicenseObject -Value $UserLicenses
          if ($UserLicenses) {
            $UserObject.LicenseObject | Add-Member -MemberType ScriptMethod -Name ToString -Value { $this.ProductName } -Force
          }
        }
        else {
          Write-Verbose -Message 'Parameter LicenseObject omitted. To receive this parameter with their nested licenses, please use DiagnosticLevel 3 or higher'
        }

        # Calling Plan
        $currentCallingPlan = ($UserLicenses | Where-Object LicenseType -EQ 'CallingPlan').ProductName
        $UserObject | Add-Member -MemberType NoteProperty -Name CurrentCallingPlan -Value $currentCallingPlan

        # PhoneSystem & PhoneSystem Provisioning Status
        $ProvisioningStatus = $($UserLicenseDetail.ServicePlans | Where-Object ServicePlanName -Like 'MCOEV*').ProvisioningStatus
        $UserObject | Add-Member -MemberType NoteProperty -Name PhoneSystemStatus -Value $ProvisioningStatus
        if ( $null -ne $UserObject.PhoneSystemStatus ) {
          $UserObject.PhoneSystemStatus | Add-Member -MemberType ScriptMethod -Name ToString -Value { [string]$this } -Force
        }
        $UserObject | Add-Member -MemberType NoteProperty -Name PhoneSystem -Value $($CsUser.FeatureTypes -contains 'PhoneSystem')
        #Info about PhoneSystemStatus (suppressing feedback if AzureAdUser is already populated)
        if ( $UserObject.PhoneSystem -and -not $ProvisioningStatus.Contains('Success') -and -not $AdUser) {
          Write-Warning -Message "'$ObjectUPN' - PhoneSystemStatus is not Success. User cannot be configured for Voice"
        }
      }

      # Adding Provisioning Parameters
      $UserObject | Add-Member -MemberType NoteProperty -Name EnterpriseVoiceEnabled -Value $CsUser.EnterpriseVoiceEnabled
      $UserObject | Add-Member -MemberType NoteProperty -Name OnlineVoiceRoutingPolicy -Value $CsUser.OnlineVoiceRoutingPolicy
      $UserObject | Add-Member -MemberType NoteProperty -Name TenantDialPlan -Value $CsUser.TenantDialPlan
      $UserObject | Add-Member -MemberType NoteProperty -Name CallingLineIdentity -Value $CsUser.CallingLineIdentity
      $UserObject | Add-Member -MemberType NoteProperty -Name LineURI -Value $CsUser.LineURI
      #endregion

      #region Adding Diagnostic Parameters
      if ($PSBoundParameters.ContainsKey('DiagnosticLevel')) {
        switch ($DiagnosticLevel) {
          { $PSItem -ge 1 } {
            # Displaying basic diagnostic parameters (Hybrid)
            $CurrentOperationID0 = 'Processing DiagnosticLevel 1 - Voice Configuration Parameters and Emergency Calling'
            Write-BetterProgress -Id 0 -Activity $ActivityID0 -Status $StatusID0 -CurrentOperation $CurrentOperationID0 -Step ($private:CountID0++) -Of $private:StepsID0
            $UserObject | Add-Member -MemberType NoteProperty -Name TelephoneNumber -Value $CsUser.TelephoneNumber
            $UserObject | Add-Member -MemberType NoteProperty -Name OnPremLineURI -Value $CsUser.OnPremLineURI
            $UserObject | Add-Member -MemberType NoteProperty -Name OnPremEnterpriseVoiceEnabled -Value $CsUser.OnPremEnterpriseVoiceEnabled
            $UserObject | Add-Member -MemberType NoteProperty -Name PrivateLine -Value $CsUser.PrivateLine
            # Query for User Location
            try {
              $VoiceUser = Get-CsOnlineVoiceUser $CsUser -ErrorAction Stop
              $UserLocation = if ( $VoiceUser ) { $VoiceUser.Location } else { $null }
              $UserAssignedAddress = if ( $UserLocation ) { (Get-CsOnlineLisLocation -LocationId $UserLocation).Description } else { $null }
            }
            catch {
              Write-Verbose -Message "User Location could not be queried: $($_.Exception.Message)"
              $UserAssignedAddress = $null
            }
            $UserObject | Add-Member -MemberType NoteProperty -Name UserAssignedAddress -Value $UserAssignedAddress
            $UserObject | Add-Member -MemberType NoteProperty -Name TeamsEmergencyCallRoutingPolicy -Value $CsUser.TeamsEmergencyCallRoutingPolicy
            $UserObject | Add-Member -MemberType NoteProperty -Name TeamsEmergencyCallingPolicy -Value $CsUser.TeamsEmergencyCallingPolicy
          }

          { $PSItem -ge 2 } {
            # Displaying extended diagnostic parameters
            $CurrentOperationID0 = 'Processing DiagnosticLevel 2 - Voice related Policies'
            Write-BetterProgress -Id 0 -Activity $ActivityID0 -Status $StatusID0 -CurrentOperation $CurrentOperationID0 -Step ($private:CountID0++) -Of $private:StepsID0
            $UserObject | Add-Member -MemberType NoteProperty -Name TeamsCallingPolicy -Value $CsUser.TeamsCallingPolicy
            $UserObject | Add-Member -MemberType NoteProperty -Name TeamsIPPhonePolicy -Value $CsUser.TeamsIPPhonePolicy
            $UserObject | Add-Member -MemberType NoteProperty -Name TeamsVdiPolicy -Value $CsUser.TeamsVdiPolicy
            $UserObject | Add-Member -MemberType NoteProperty -Name TeamsUpgradePolicy -Value $CsUser.TeamsUpgradePolicy
            $UserObject | Add-Member -MemberType NoteProperty -Name OnlineDialOutPolicy -Value $CsUser.OnlineDialOutPolicy
            $UserObject | Add-Member -MemberType NoteProperty -Name OnlineVoicemailPolicy -Value $CsUser.OnlineVoicemailPolicy
            $UserObject | Add-Member -MemberType NoteProperty -Name OnlineAudioConferencingRoutingPolicy -Value $CsUser.OnlineAudioConferencingRoutingPolicy
            $UserObject | Add-Member -MemberType NoteProperty -Name VoiceRoutingPolicy -Value $CsUser.VoiceRoutingPolicy
            $UserObject | Add-Member -MemberType NoteProperty -Name TeamsUpgradeEffectiveMode -Value $CsUser.TeamsUpgradeEffectiveMode
          }

          { $PSItem -ge 3 } {
            # Querying AD Object (if Diagnostic Level is 3 or higher)
            $CurrentOperationID0 = 'Processing DiagnosticLevel 3 - Querying AzureAd User'
            Write-BetterProgress -Id 0 -Activity $ActivityID0 -Status $StatusID0 -CurrentOperation $CurrentOperationID0 -Step ($private:CountID0++) -Of $private:StepsID0
            if ( -not $AdUser ) {
              try {
                $AdUser = Get-AzureADUser -ObjectId "$ObjectUPN" -WarningAction SilentlyContinue -ErrorAction Stop
              }
              catch {
                if ( $AdUser -ne $CsUser ) {
                  Write-Warning -Message "'$ObjectUPN' not found in AzureAD. Some data will not be available"
                }
              }
            }

            # Displaying advanced diagnostic parameters
            $CurrentOperationID0 = 'Processing DiagnosticLevel 3 - AzureAd Parameters, Status'
            Write-BetterProgress -Id 0 -Activity $ActivityID0 -Status $StatusID0 -CurrentOperation $CurrentOperationID0 -Step ($private:CountID0++) -Of $private:StepsID0
            $UserObject | Add-Member -MemberType NoteProperty -Name HostingProvider -Value $CsUser.HostingProvider
            #$UserObject | Add-Member -MemberType NoteProperty -Name UserValidationErrors -Value $CsUser.UserValidationErrors
            $UserObject | Add-Member -MemberType NoteProperty -Name TeamsVoiceRoute -Value $CsUser.TeamsVoiceRoute # Parked here as low priority
            $UserObject | Add-Member -MemberType NoteProperty -Name AdAccountEnabled -Value $AdUser.AccountEnabled
            $UserObject | Add-Member -MemberType NoteProperty -Name CsAccountEnabled -Value $CsUser.IsSipEnabled
            $UserObject | Add-Member -MemberType NoteProperty -Name CsAccountIsValid -Value $CsUser.IsValid
            $UserObject | Add-Member -MemberType NoteProperty -Name CsWhenCreated -Value $CsUser.WhenCreated
            $UserObject | Add-Member -MemberType NoteProperty -Name CsWhenChanged -Value $CsUser.WhenChanged
            $UserObject | Add-Member -MemberType NoteProperty -Name AdObjectType -Value $AdUser.ObjectType
            $UserObject | Add-Member -MemberType NoteProperty -Name AdObjectClass -Value $CsUser.ObjectClass
          }
          { $PSItem -ge 4 } {
            # Displaying all of CsOnlineUser (previously omitted)
            $CurrentOperationID0 = 'Processing DiagnosticLevel 4 - AzureAd Parameters, DirSync'
            Write-BetterProgress -Id 0 -Activity $ActivityID0 -Status $StatusID0 -CurrentOperation $CurrentOperationID0 -Step ($private:CountID0++) -Of $private:StepsID0
            $CsUserDirSyncEnabled = ( $AdUser.DirSyncEnabled -or $CsUser.UserDirSyncEnabled ) # Renamed parameters
            $UserObject | Add-Member -MemberType NoteProperty -Name UserDirSyncEnabled -Value $CsUserDirSyncEnabled
            $UserObject | Add-Member -MemberType NoteProperty -Name LastDirSyncTime -Value $AdUser.LastDirSyncTime
            $UserObject | Add-Member -MemberType NoteProperty -Name AdDeletionTimestamp -Value $AdUser.DeletionTimestamp
            $UserObject | Add-Member -MemberType NoteProperty -Name CsSoftDeletionTimestamp -Value $CsUser.SoftDeletionTimestamp
            $UserObject | Add-Member -MemberType NoteProperty -Name CsPendingDeletion -Value $CsUser.PendingDeletion
            $UserObject | Add-Member -MemberType NoteProperty -Name HideFromAddressLists -Value $CsUser.HideFromAddressLists
            $UserObject | Add-Member -MemberType NoteProperty -Name OnPremHideFromAddressLists -Value $CsUser.OnPremHideFromAddressLists
            $UserObject | Add-Member -MemberType NoteProperty -Name OriginatingServer -Value $CsUser.OriginatingServer
            $UserObject | Add-Member -MemberType NoteProperty -Name ServiceInstance -Value $CsUser.ServiceInstance
            $UserObject | Add-Member -MemberType NoteProperty -Name SipProxyAddress -Value $CsUser.SipProxyAddress
          }
        }
      }
      #endregion

      # Output
      Write-Progress -Id 0 -Activity $ActivityID0 -Completed
      Write-Output $UserObject
    }
  } #process

  end {
    Write-Verbose -Message "[END ] $($MyInvocation.MyCommand)"
  } #end
} #Get-TeamsUserVoiceConfig