Nectar10.psm1

<#
These series of PowerShell functions allow administrators to automate many functions in Nectar 10 and the Endpoint Client Controller that are otherwise difficult or time-consuming to perform in the UI.
 
To install, follow the instructions at https://support.nectarcorp.com/docs/powershell
 
If you want help with a particular command, type Get-Help <commandname> -Full (ie. Get-Help Connect-NectarCloud -Full). Full documentation is available at https://support.nectarcorp.com/docs/powershell
#>



#################################################################################################################################################
#################################################################################################################################################
## ##
## Nectar 10 Functions ##
## ##
#################################################################################################################################################
#################################################################################################################################################

#################################################################################################################################################
# #
# Tenant Connection Functions #
# #
#################################################################################################################################################

Function Connect-NectarCloud {
    <#
        .SYNOPSIS
        Connects to Nectar 10 cloud and store the credentials for later use.
 
        .DESCRIPTION
        Connects to Nectar 10 cloud and store the credentials for later use.
         
        .PARAMETER CloudFQDN
        The FQDN of the Nectar 10 cloud.
 
        .PARAMETER TenantName
        The name of a Nectar 10 cloud tenant to connect to and use for subsequent commands. Only useful for multi-tenant deployments
         
        .PARAMETER Credential
        The credentials used to access the Nectar 10 UI. Normally in username@domain.com format
         
        .PARAMETER StoredCredentialTarget
        Use stored credentials saved via New-StoredCredential. Requires prior installation of CredentialManager module via Install-Module CredentialManager, and running:
        Get-Credential | New-StoredCredential -Target MyN10Creds -Persist LocalMachine
         
        .PARAMETER EnvFromFile
        Use a CSV file called N10EnvList.csv located in the user's default Documents folder to show a list of environments to select from. Run [Environment]::GetFolderPath("MyDocuments") to find your default document folder.
        This parameter is only available if N10EnvList.csv is found in the user's default Documents folder (ie: C:\Users\username\Documents)
        Also sets the default stored credential target to use for the selected environment. Requires prior installation and configuration of CredentialManager PS add-in.
        N10EnvList.csv must have a header with three columns defined as "Environment, DefaultTenant, StoredCredentialTarget".
        Each environment and StoredCredentialTarget (if used) should be on their own separate lines
         
        .EXAMPLE
        $Cred = Get-Credential
        Connect-NectarCloud -Credential $cred -CloudFQDN contoso.nectar.services
        Connects to the contoso.nectar.services Nectar 10 cloud using the credentials supplied to the Get-Credential command
         
        .EXAMPLE
        Connect-NectarCloud -CloudFQDN contoso.nectar.services -StoredCredentialTarget MyN10Creds
        Connects to contoso.nectar.services Nectar 10 cloud using previously stored credentials called MyN10Creds
         
        .NOTES
        Version 1.4
    #>

    
    [Alias("cnc")]
    Param (
        [Parameter(ValueFromPipeline, Mandatory=$False)]
        [ValidateScript ({
            If ($_ -Match "^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.]+$") {
                $True
            } 
            Else {
                Throw "ERROR: Nectar 10 cloud name must be in FQDN format."
            }
        })]
        [string] $CloudFQDN,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Management.Automation.Credential()]
        [PSCredential] $Credential,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$StoredCredentialTarget
    )
    DynamicParam {
        $DefaultDocPath = [Environment]::GetFolderPath("MyDocuments")
        $EnvPath = "$DefaultDocPath\N10EnvList.csv"
        If (Test-Path $EnvPath -PathType Leaf) {
            # Set the dynamic parameters' name
            $ParameterName = 'EnvFromFile'
            
            # Create the dictionary
            $RuntimeParameterDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
         
            # Create the collection of attributes
            $AttributeCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
                    
            # Create and set the parameters' attributes
            $ParameterAttribute = New-Object System.Management.Automation.ParameterAttribute
            $ParameterAttribute.Mandatory = $False
            $ParameterAttribute.Position = 1
         
            # Add the attributes to the attributes collection
            $AttributeCollection.Add($ParameterAttribute)
         
            # Generate and set the ValidateSet
            $EnvSet = Import-Csv -Path $EnvPath
            $ValidateSetAttribute = New-Object System.Management.Automation.ValidateSetAttribute($EnvSet.Environment)
         
            # Add the ValidateSet to the attributes collection
            $AttributeCollection.Add($ValidateSetAttribute)
         
            # Create and return the dynamic parameter
            $RuntimeParameter = New-Object System.Management.Automation.RuntimeDefinedParameter($ParameterName, [string], $AttributeCollection)
            $RuntimeParameterDictionary.Add($ParameterName, $RuntimeParameter)
            Return $RuntimeParameterDictionary
        }
    }
    
    Begin {
        # Bind the dynamic parameter to a friendly variable
        If (Test-Path $EnvPath -PathType Leaf) {
            If ($PsBoundParameters[$ParameterName]) {
                $CloudFQDN = $PsBoundParameters[$ParameterName]
                Write-Verbose "CloudFQDN: $CloudFQDN"
                
                # Get the array position of the selected environment
                $EnvPos = $EnvSet.Environment.IndexOf($CloudFQDN)
                
                # Check for default tenant in N10EnvList.csv and use if available, but don't override if user explicitly set the TenantName
                If (!$PsBoundParameters['TenantName']) {
                    $TenantName = $EnvSet[$EnvPos].DefaultTenant
                    Write-Verbose "DefaultTenant: $TenantName"
                }
                
                # Check for stored credential target in N10EnvList.csv and use if available
                $StoredCredentialTarget = $EnvSet[$EnvPos].StoredCredentialTarget
                Write-Verbose "StoredCredentialTarget: $StoredCredentialTarget"
            }
        }
    }
    Process {
        # Need to force TLS 1.2, if not already set
        If ([Net.ServicePointManager]::SecurityProtocol -ne 'Tls12') { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 }
        
        # Ask for the tenant name if global Nectar tenant variable not available and not entered on command line
        If ((-not $Global:NectarCloud) -And (-not $CloudFQDN)) {
            $CloudFQDN = Read-Host "Enter the Nectar 10 cloud FQDN"
        }
        ElseIf (($Global:NectarCloud) -And (-not $CloudFQDN)) {
            $CloudFQDN = $Global:NectarCloud
        }
        
        $RegEx = "^(?:http(s)?:\/\/)?([\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.]+)$"
        $FQDNMatch = Select-String -Pattern $Regex -InputObject $CloudFQDN
        $CloudFQDN = $FQDNMatch.Matches.Groups[2].Value
        
        # Ask for credentials if global Nectar creds aren't available
        If (((-not $Global:NectarCred) -And (-not $Credential)) -Or (($Global:NectarCloud -ne $CloudFQDN) -And (-Not $Credential)) -And (-Not $StoredCredentialTarget)) {
            $Credential = Get-Credential
        }
        ElseIf ($Global:NectarCred -And (-not $Credential)) {
            $Credential = $Global:NectarCred
        }
        
        # Pull stored credentials if specified
        If ($StoredCredentialTarget) {
            Try {
                $Credential = Get-StoredCredential -Target $StoredCredentialTarget
            }
            Catch {
                Write-Error "Cannot find stored credential for target: $StoredCredentialTarget"
            }
        }
        
        If ((-not $Global:NectarCred) -Or (-not $Global:NectarCloud) -Or ($Global:NectarCloud -ne $CloudFQDN)) {
            # First check and notify if updated N10 PS module available
            [decimal]$InstalledN10Ver = (Get-InstalledModule -Name Nectar10 -ErrorAction SilentlyContinue).Version
            
            If ($InstalledN10Ver -gt 0) {
                [decimal]$LatestN10Ver = (Find-Module Nectar10).Version
                If ($LatestN10Ver -gt $InstalledN10Ver) {
                    Write-Host "=============== Nectar 10 PowerShell module version $LatestN10Ver available ===============" -ForegroundColor Yellow
                    Write-Host "You are running version $InstalledN10Ver. Type " -ForegroundColor Yellow -NoNewLine
                    Write-Host 'Update-Module Nectar10' -ForegroundColor Green -NoNewLine
                    Write-Host ' to update.' -ForegroundColor Yellow
                }
            }
            
            # Attempt connection to tenant
            $WebRequest = Invoke-WebRequest -Uri "https://$CloudFQDN/dapi/info/network/types" -Method GET -Credential $Credential -UseBasicParsing -SessionVariable NectarSession
            
            If ($WebRequest.StatusCode -ne 200) {
                Write-Error "Could not connect to $CloudFQDN using $($Credential.UserName)"
            }
            Else {
                Write-Host -ForegroundColor Green "Successful connection to " -NoNewLine
                Write-Host -ForegroundColor Yellow "https://$CloudFQDN" -NoNewLine
                Write-Host -ForegroundColor Green " using " -NoNewLine
                Write-Host -ForegroundColor Yellow ($Credential).UserName
                $Global:NectarCloud = $CloudFQDN
                $Global:NectarCred = $Credential
                $Global:NectarSession = $NectarSession
                
                # If there is only one available tenant, assign that to the NectarTenantName global variable
                $TenantList = $WebRequest | ConvertFrom-Json
                If ($TenantList.Count -eq 1) { $Global:NectarTenantName = $TenantList }            
            }
        }
        
        # Check to see if tenant name was entered and set global variable, if valid.
        If ($TenantName) {
            $TenantList = Invoke-RestMethod -Method GET -Credential $Global:NectarCred -uri "https://$Global:NectarCloud/aapi/tenant"
            Try {
                If ($TenantList -Contains $TenantName) {
                    $Global:NectarTenantName = $TenantName
                    Write-Host -ForegroundColor Green "Successsfully set the tenant name to " -NoNewLine
                    Write-Host -ForegroundColor Yellow "$TenantName" -NoNewLine
                    Write-Host -ForegroundColor Green ". This name will be used in all subsequent commands."
                }
                Else {
                    $TenantList | %{$TList += ($(if($TList){", "}) + $_)}
                    Write-Error "Could not find a tenant with the name $TenantName on https://$Global:NectarCloud. Select one of $TList"
                }
            }
            Catch {
                Write-Error "Invalid tenant name on https://$Global:NectarCloud"
            }
        }
        ElseIf ($PSBoundParameters.ContainsKey('TenantName')) { # Remove the NectarTenantName global variable only if TenantName is explicitly set to NULL
            Remove-Variable NectarTenantName -Scope Global -ErrorAction:SilentlyContinue
        }
    }
}


Function Connect-NectarCloudSSO {
    <#
        .SYNOPSIS
        Connects to Nectar 10 cloud via SSO and store the crendentials for later use.
 
        .DESCRIPTION
        Connects to Nectar 10 cloud via SSO and store the crendentials for later use.
         
        .PARAMETER CloudFQDN
        The FQDN of the Nectar 10 cloud.
 
        .PARAMETER DomainName
        The name of the SSO domain to use for connecting to N10
         
 
        .NOTES
        Version 0.1 (not working yet)
    #>

    
    [Alias("cncs")]
    param (
        [Parameter(ValueFromPipeline, Mandatory=$False)]
        [ValidateScript ({
            If ($_ -Match "(?=^.{4,253}$)(^((?!-)[a-zA-Z0-9-]{1,63}(?<!-)\.)+[a-zA-Z]{2,63}$)") {
                $True
            } 
            Else {
                Throw "ERROR: Nectar 10 cloud name must be in FQDN format."
            }
        })]
        [string] $CloudFQDN,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [string]$DomainName
    )
    
    # Need to force TLS 1.2, if not already set
    If ([Net.ServicePointManager]::SecurityProtocol -ne 'Tls12') { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 }
    
    # Ask for the tenant name if global Nectar tenant variable not available and not entered on command line
    If ((-not $Global:NectarCloud) -And (-not $CloudFQDN)) {
        $CloudFQDN = Read-Host "Enter the Nectar 10 cloud FQDN"
    }
    ElseIf (($Global:NectarCloud) -And (-not $CloudFQDN)) {
        $CloudFQDN = $Global:NectarCloud
    }
    
    # Check that the entered domain is valid
    $DomainCheck = Invoke-WebRequest -Uri "https://$CloudFQDN/adminapi/user/sso/domain/exists?domain=$DomainName" -Method GET
    
    If ($DomainCheck.content -eq 'True') {
        $WebRedirect = Invoke-WebRequest -Uri "https://$CloudFQDN/saml/login?domain=$DomainName" -Method POST -MaximumRedirection 0 -SessionVariable RequestCookie -UseDefaultCredentials
        $SAMLRedirectURL = $WebRedirect.Forms[0].action
        $SAMLRequest = $WebRedirect.Forms[0].fields.Values[0]
        
        $SAML = Invoke-WebRequest -Uri $SAMLRedirectURL -Method POST -MaximumRedirection 0 -ContentType 'application/x-www-form-urlencoded' -WebSession $RequestCookie
        
        
    }
    Else {
        Write-Error "The domain name $DomainName is not valid for $CloudFQDN."
    }
    
    # This opens the auth provider portal in a web browser and progresses from there to the N10 web UI.
    # Need to figure out how to intercept the auth token for PS usage.
    
}



Function Disconnect-NectarCloud {
    <#
        .SYNOPSIS
        Disconnects from any active Nectar 10 connection
         
        .DESCRIPTION
        Essentially deletes any stored credentials and FQDN from global variables
 
        .EXAMPLE
        Disconnect-NectarCloud
        Disconnects from all active connections to Nectar 10 tenants
 
        .NOTES
        Version 1.1
    #>

    [Alias("dnc")]
    [cmdletbinding()]
    param ()
    
    Remove-Variable NectarCred -Scope Global -ErrorAction:SilentlyContinue
    Remove-Variable NectarCloud -Scope Global -ErrorAction:SilentlyContinue
    Remove-Variable NectarSession -Scope Global -ErrorAction:SilentlyContinue
    Remove-Variable NectarTenantName -Scope Global -ErrorAction:SilentlyContinue
}



Function Get-NectarCloudInfo {
    <#
        .SYNOPSIS
        Shows information about the active Nectar 10 connection
         
        .DESCRIPTION
        Shows information about the active Nectar 10 connection
 
        .EXAMPLE
        Get-NectarCloud
 
        .NOTES
        Version 1.1
    #>

    
    [Alias("gnci")]
    [cmdletbinding()]
    param ()
    
    $CloudInfo = "" | Select-Object -Property CloudFQDN, Credential
    $CloudInfo.CloudFQDN = $Global:NectarCloud
    $CloudInfo.Credential = ($Global:NectarCred).UserName
    $CloudInfo | Add-Member -TypeName 'Nectar.CloudInfo'
    
    Try {
        $TenantCount = Get-NectarTenantNames
        If ($TenantCount.Count -gt 1) {
            If ($Global:NectarTenantName) {
                $CloudInfo | Add-Member -NotePropertyName 'TenantName' -NotePropertyValue $Global:NectarTenantName
            }
            Else {
                $CloudInfo | Add-Member -NotePropertyName 'TenantName' -NotePropertyValue '<Not Set>'
            }
        }
    }
    Catch {
    }
    
    Return $CloudInfo
}


Function Get-NectarTenantNames {
    <#
        .SYNOPSIS
        Shows all the available Nectar tenants on the cloud host.
         
        .DESCRIPTION
        Shows all the available Nectar tenants on the cloud host. Only available for multi-tenant deployments.
 
        .EXAMPLE
        Get-NectarTenantNames
 
        .NOTES
        Version 1.0
    #>

    
    [Alias("gntn")]
    [cmdletbinding()]
    param ()
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        Try {
            $JSON = Invoke-RestMethod -Method GET -WebSession $Global:NectarSession -uri "https://$Global:NectarCloud/aapi/tenant"
            $TenantList = @()
             
            Foreach ($Item in $JSON) {
                $PSObject = New-Object PSObject -Property @{
                    TenantName = $Item
                }
                $TenantList += $PSObject
            }
            $TenantList
        }
        Catch {
            Write-Error 'No tenants found, or insufficient permissions.'
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



Function Get-NectarTenantDatasources {
    <#
        .SYNOPSIS
        Shows all the datasources available on a given tenant.
         
        .DESCRIPTION
        Shows all the datasources available on a given tenant.
 
        .EXAMPLE
        Get-NectarTenantDatasources
 
        .NOTES
        Version 1.0
    #>

    
    [Alias("gntdc")]
    [cmdletbinding()]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        Try {
            If (!$TenantName) { $TenantName = Get-NectarDefaultTenantName }

            $JSON = Invoke-RestMethod -Method GET -WebSession $Global:NectarSession -uri "https://$Global:NectarCloud/aapi/tenant/datasources?tenant=$TenantName"
            Return $JSON.data
        }
        Catch {
            Write-Error 'No tenant datasources found, or insufficient permissions.'
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



Function Get-NectarTenantPlatforms {
    <#
        .SYNOPSIS
        Shows all the platforms available on a given tenant.
         
        .DESCRIPTION
        Shows all the platforms available on a given tenant.
 
        .EXAMPLE
        Get-NectarTenantPlatforms
 
        .NOTES
        Version 1.0
    #>

    
    [Alias("gntp")]
    [cmdletbinding()]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        Try {
            If (!$TenantName) { $TenantName = Get-NectarDefaultTenantName }

            $JSON = Invoke-RestMethod -Method GET -WebSession $Global:NectarSession -uri "https://$Global:NectarCloud/aapi/tenant/platforms?tenant=$TenantName"

            ForEach ($Item in $JSON) {
                $PlatformList = [pscustomobject][ordered]@{
                    TenantName = $TenantName
                    Platform = $Item.platform
                    Supported = $Item.supported
                }
                $PlatformList
            }
        }
        Catch {
            Write-Error 'No tenant platforms found, or insufficient permissions.'
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



Function Get-NectarTenantSSOConfig {
    <#
        .SYNOPSIS
        Shows the SSO config for a given tenant.
         
        .DESCRIPTION
        Shows the SSO config for a given tenant.
 
        .EXAMPLE
        Get-NectarTenantSSOConfig
 
        .NOTES
        Version 1.0
    #>

    
    [Alias("gntsc")]
    [cmdletbinding()]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        Try {
            If (!$TenantName) { $TenantName = Get-NectarDefaultTenantName }

            $JSON = Invoke-RestMethod -Method GET -WebSession $Global:NectarSession -uri "https://$Global:NectarCloud/aapi/client/sso-config?tenant=$TenantName"
            Return $JSON

            ForEach ($Item in $JSON) {
                $PlatformList = [pscustomobject][ordered]@{
                    TenantName = $TenantName
                    Platform = $Item.platform
                    Supported = $Item.supported
                }
                $PlatformList
            }
        }
        Catch {
            Write-Error 'No tenant platforms found, or insufficient permissions.'
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}




#################################################################################################################################################
# #
# Tenant Email Domain Functions #
# #
#################################################################################################################################################

Function Get-NectarEmailDomain {
    <#
        .SYNOPSIS
        Returns a list of Nectar 10 allowed email domains that can be used for login IDs.
         
        .DESCRIPTION
        Returns a list of Nectar 10 allowed email domains that can be used for login IDs.
         
        .PARAMETER TenantName
        The name of the Nectar 10 tenant. Used in multi-tenant configurations.
 
        .PARAMETER ResultSize
        The number of results to return. Defaults to 1000.
         
        .EXAMPLE
        Get-NectarEmailDomain
        Returns all the allowed email domains for the logged in tenant.
 
        .NOTES
        Version 1.1
    #>

    
    [Alias("gne")]
    Param (
        [Parameter(Mandatory=$False)]
        [string]$SearchQuery,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,100000)]
        [int]$ResultSize = 1000
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
            
            $JSON = Invoke-RestMethod -Method GET -WebSession $Global:NectarSession -uri "https://$Global:NectarCloud/aapi/client/domains?searchQuery=$SearchQuery&tenant=$TenantName&pageSize=$ResultSize"

            If ($JSON.domainNames) { Return $JSON.domainNames }
            If ($JSON.elements) { Return $JSON.elements }
        }
        Catch {
            Write-Error 'No email domains found, or insufficient permissions.'
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}


Function New-NectarEmailDomain {
    <#
        .SYNOPSIS
        Add a new allowed email domain that can be used for login IDs.
         
        .DESCRIPTION
        Add a new allowed email domain that can be used for login IDs.
         
        .PARAMETER EmailDomain
        The email domain to add to the tenant.
 
        .PARAMETER TenantName
        The name of the Nectar 10 tenant. Used in multi-tenant configurations.
         
        .EXAMPLE
        New-NectarEmailDomain -EmailDomain contoso.com
        Adds the contoso.com email domain to the logged in Nectar 10 tenant.
 
        .NOTES
        Version 1.1
    #>

    
    [Alias("nne")]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [string]$EmailDomain,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
        
        $URI = "https://$Global:NectarCloud/aapi/client/domains?tenant=$TenantName"
        
        $JSONBody = $EmailDomain
        
        Try {
            $JSON = Invoke-RestMethod -Method POST -WebSession $Global:NectarSession -uri $URI -Body $JSONBody -ContentType 'application/json; charset=utf-8'
            Write-Verbose "Successfully added $EmailDomain as an allowed email domain."
        }
        Catch {
            Write-Error "Unable to add $EmailDomain to list of allowed email domains."
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



Function Remove-NectarEmailDomain {
    <#
        .SYNOPSIS
        Remove an allowed email domain that can be used for login IDs.
         
        .DESCRIPTION
        Remove an allowed email domain that can be used for login IDs.
         
        .PARAMETER EmailDomain
        The email domain to remove from the tenant.
 
        .PARAMETER TenantName
        The name of the Nectar 10 tenant. Used in multi-tenant configurations.
         
        .EXAMPLE
        Remove-NectarEmailDomain -EmailDomain contoso.com
        Removes the contoso.com email domain from the list of allowed domains on the logged in Nectar 10 tenant.
 
        .NOTES
        Version 1.1
    #>

    [Alias("rne")]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias("domainName")]
        [string]$EmailDomain,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,    
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias("id")]
        [int]$Identity
    )
    
    Begin {
        Connect-NectarCloud
    }        
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }

        If ($EmailDomain -and !$Identity) {
            $Identity = (Get-NectarEmailDomain -TenantName $TenantName -SearchQuery $EmailDomain -ResultSize 1 -ErrorVariable GetDomainError).ID
        }

        If (!$GetDomainError) {
            $URI = "https://$Global:NectarCloud/aapi/client/domains/$Identity/?tenant=$TenantName"

            Try {
                $JSON = Invoke-RestMethod -Method DELETE -WebSession $Global:NectarSession -uri $URI
                Write-Verbose "Successfully deleted $EmailDomain from list of allowed email account domains."
            }
            Catch {
                Write-Error "Unable to delete email domain $EmailDomain. Ensure you typed the name of the email domain correctly."
                Get-JSONErrorStream -JSONResponse $_
            }
        }
    }
}




#################################################################################################################################################
# #
# Tenant Admin Functions #
# #
#################################################################################################################################################

Function Get-NectarAdmin {
    <#
        .SYNOPSIS
        Get information about 1 or more Nectar 10 users.
         
        .DESCRIPTION
        Get information about 1 or more Nectar 10 users.
 
        .PARAMETER SearchQuery
        A full or partial match of the user's first or last name or email address
 
        .PARAMETER TenantName
        The name of the Nectar 10 tenant. Used in multi-tenant configurations.
         
        .PARAMETER ResultSize
        The number of results to return. Defaults to 1000.
         
        .EXAMPLE
        Get-NectarAdmin -SearchQuery tferguson@contoso.com
        Returns information about the user tferguson@contoso.com
         
        .NOTES
        Version 1.1
    #>

    
    [Alias("gna")]
    Param (
        [Parameter(Mandatory=$False)]
        [string]$SearchQuery,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,100000)]
        [int]$ResultSize = 1000
    )    
    
    Begin {
        Connect-NectarCloud
    }        
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
        
        $URI = "https://$Global:NectarCloud/aapi/users?searchQuery=$SearchQuery&tenant=$TenantName&pageSize=$ResultSize"
        Try {
            $JSON = Invoke-RestMethod -Method POST -WebSession $Global:NectarSession -uri $URI
            If ($TenantName) {$JSON.elements | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty} # Add the tenant name to the output which helps pipelining
            $JSON.elements | Add-Member -TypeName 'Nectar.AdminList'            
            Return $JSON.elements
        }
        Catch {
            Write-Error "Unable to get user details."
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}


Function Set-NectarAdmin {
    <#
        .SYNOPSIS
        Update 1 or more Nectar 10 admin accounts.
         
        .DESCRIPTION
        Update 1 or more Nectar 10 admin accounts.
         
        .PARAMETER FirstName
        The first name of the user
         
        .PARAMETER LastName
        The last name of the user
 
        .PARAMETER EmailAddress
        The email address of the user
         
        .PARAMETER AdminStatus
        True if Admin, False if not. Used when importing many admin accounts via CSV
         
        .PARAMETER IsAdmin
        Include if user is to be an admin. If not present, then user will be read-onl
 
        .PARAMETER TenantName
        The name of the Nectar 10 tenant. Used in multi-tenant configurations.
                 
        .PARAMETER Identity
        The numerical identity of the user
         
        .EXAMPLE
        Set-NectarAdmin Identity 233
         
        .NOTES
        Version 1.1
    #>

    
    [Alias("sna")]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias("name")]
        [string]$FirstName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$LastName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias("email")]
        [string]$EmailAddress,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias("admin")]
        [string]$AdminStatus,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [switch]$IsAdmin,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$Role,        
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias("id")]
        [String]$Identity
    )
    
    Begin {
        Connect-NectarCloud
    }        
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
        
        # If ($IsAdmin -and !$AdminStatus) {
            # $AdminStatus = "true"
        # }
        # ElseIf (!$IsAdmin -and !$AdminStatus) {
            # $AdminStatus = "false"
        # }
        
        If ($EmailAddress -And !$Identity) {
            $UserInfo = Get-NectarAdmin -SearchQuery $EmailAddress -Tenant $TenantName -ResultSize 1
            $Identity = $UserInfo.id
        }
        
        If (-not $FirstName) {$FirstName = $UserInfo.firstName}
        If (-not $LastName) {$LastName = $UserInfo.lastName}
        If (-not $EmailAddress) {$EmailAddress = $UserInfo.email}
        If (-not $IsAdmin -and !$AdminStatus) {$AdminStatus = $UserInfo.admin}
            
        $URI = "https://$Global:NectarCloud/aapi/user/$Identity/?tenant=$TenantName"

        $Body = @{
            id = $Identity
            email = $EmailAddress
            firstName = $FirstName
            lastName = $LastName
# isAdmin = $AdminStatus
            userRoleName = $Role
# userStatus = "ACTIVE"
        }
        
        $JSONBody = $Body | ConvertTo-Json

        Try {
            Write-Verbose $JSONBody
            $JSON = Invoke-RestMethod -Method PUT -WebSession $Global:NectarSession -uri $URI -Body $JSONBody -ContentType 'application/json; charset=utf-8'
        }
        Catch {
            Write-Error "Unable to apply changes for user $EmailAddress."
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}


Function New-NectarAdmin {
    <#
        .SYNOPSIS
        Create a new Nectar 10 admin account.
         
        .DESCRIPTION
        Create a new Nectar 10 admin account.
 
        .PARAMETER FirstName
        The user's first name
 
        .PARAMETER LastName
        The user's last name
 
        .PARAMETER EmailAddress
        The user's email address
         
        .PARAMETER Password
        The password to assign to the new account
         
        .PARAMETER AdminStatus
        True if Admin, False if not. Used when importing many admin accounts via CSV
         
        .PARAMETER IsAdmin
        Include if user is to be an admin. If not present, then user will be read-only
         
        .PARAMETER TenantName
        The name of the Nectar 10 tenant. Used in multi-tenant configurations.
         
        .EXAMPLE
        New-NectarAdmin -FirstName Turd -LastName Ferguson -Email tferguson@contoso.com -Password VeryStrongPassword -IsAdmin
        Creates a new admin user called Turd Ferguson
         
        .EXAMPLE
        Import-Csv .\Users.csv | New-NectarAdmin
        Creates admin accounts using a CSV file as input. CSV file must have the following headers: FirstName,LastName,Password,Email,AdminStatus (Use True/False for AdminStatus)
        .
        .NOTES
        Version 1.2
    #>

    
    [Alias("nna")]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$FirstName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$LastName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [Alias("email")]
        [string]$EmailAddress,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$Password,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$AdminStatus,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [switch]$IsAdmin,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$Role,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName        
    )    

    Begin {
        Connect-NectarCloud
    }        
    Process {
        If ($IsAdmin -and !$AdminStatus) {
            $AdminStatus = "true"
        }
        ElseIf (!$IsAdmin -and !$AdminStatus) {
            $AdminStatus = "false"
        }

        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
                
        $URI = "https://$Global:NectarCloud/aapi/user?tenant=$TenantName"
        $Body = @{
            email = $EmailAddress
            firstName = $FirstName
            lastName = $LastName
            isAdmin = $AdminStatus
            userRoleName = $Role
            userStatus = "ACTIVE"
        }
        
        If ($Password) { $Body.Add('password',$Password) }
            
        $JSONBody = $Body | ConvertTo-Json

        Try {
            Write-Verbose $JSONBody
            $JSON = Invoke-RestMethod -Method POST -WebSession $Global:NectarSession -uri $URI -Body $JSONBody -ContentType 'application/json; charset=utf-8'
        }
        Catch {
            Write-Error "Unable to create admin account $EmailAddress."
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}


Function Remove-NectarAdmin {
    <#
        .SYNOPSIS
        Removes one or more Nectar 10 admin account.
 
        .DESCRIPTION
        Removes one or more Nectar 10 admin account.
         
        .PARAMETER EmailAddress
        The email address of the admin account to remove.
 
        .PARAMETER TenantName
        The name of the Nectar 10 tenant. Used in multi-tenant configurations.
             
        .PARAMETER Identity
        The numerical ID of the admin account to remove. Can be obtained via Get-NectarAdmin and pipelined to Remove-NectarAdmin
         
        .EXAMPLE
        Remove-NectarAdmin tferguson@nectarcorp.com
        Removes the admin account tferguson@nectarcorp.com
         
        .NOTES
        Version 1.1
    #>

    
    [Alias("rna")]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias("email")]
        [string]$EmailAddress,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias("id")]
        [string]$Identity
    )
    
    Begin {
        Connect-NectarCloud
    }        
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
        
        If ($EmailAddress -and !$Identity) {
            $Identity = (Get-NectarAdmin -SearchQuery $EmailAddress -Tenant $TenantName -ResultSize 1 -ErrorVariable GetUserError).ID
        }
            
        If (!$GetUserError) {
            $URI = "https://$Global:NectarCloud/aapi/user/$Identity/?tenant=$TenantName"
            
            Try {
                $JSON = Invoke-RestMethod -Method DELETE -WebSession $Global:NectarSession -uri $URI
                Write-Verbose "Successfully deleted $EmailAddress from admin account list."
            }
            Catch {
                Write-Error "Unable to delete user $EmailAddress. Ensure you typed the name of the user correctly."
                Get-JSONErrorStream -JSONResponse $_
            }
        }
    }
}



#################################################################################################################################################
# #
# Tenant Location Functions #
# #
#################################################################################################################################################

Function Get-NectarLocation {
    <#
        .SYNOPSIS
        Returns a list of Nectar 10 locations
         
        .DESCRIPTION
        Returns a list of Nectar 10 locations
 
        .PARAMETER SearchQuery
        The name of the location to get information on based on either network, networkName, City, StreetAddress, State, SiteName or SiteCode. Can be a partial match, and may return more than one entry.
         
        .PARAMETER TenantName
        The name of the Nectar 10 tenant. Used in multi-tenant configurations.
                 
        .PARAMETER ResultSize
        The number of results to return. Defaults to 1000.
         
        .EXAMPLE
        Get-NectarLocation
        Returns the first 10 locations
         
        .EXAMPLE
        Get-NectarLocation -ResultSize 100
        Returns the first 100 locations
 
        .EXAMPLE
        Get-NectarLocation -LocationName Location2
        Returns up to 10 locations that contains "location2" anywhere in the name. The search is not case-sensitive. This example would return Location2, Location20, Location214, MyLocation299 etc
 
        .EXAMPLE
        Get-NectarLocation -LocationName ^Location2
        Returns up to 10 locations that starts with "location2" in the name. The search is not case-sensitive. This example would return Location2, Location20, Location214 etc, but NOT MyLocation299
 
        .EXAMPLE
        Get-NectarLocation -LocationName ^Location2$
        Returns a location explicitly named "Location2". The search is not case-sensitive.
 
        .NOTES
        Version 1.1
    #>

    
    [Alias("gnl")]
    Param (
        [Parameter(Mandatory=$False)]
        [string]$SearchQuery,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,100000)]
        [int]$ResultSize = 5000
    )
    
    Begin {
        Connect-NectarCloud
    }        
    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
            
            $URI = "https://$Global:NectarCloud/aapi/config/locations?pageNumber=1&tenant=$TenantName&pageSize=$ResultSize&searchQuery=$SearchQuery"
        
            $JSON = Invoke-RestMethod -Method GET -WebSession $Global:NectarSession -uri $URI
            
            If (!$JSON.elements) {
                Write-Error "Location $SearchQuery not found."
            }
            Else {
                If ($TenantName) { $JSON.elements | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty } # Add the tenant name to the output which helps pipelining
                $JSON.elements | Add-Member -TypeName 'Nectar.LocationList'
                Return $JSON.elements
            }
        }
        Catch {
            Write-Error "Unable to get location details."
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}


Function Set-NectarLocation {
    <#
        .SYNOPSIS
        Update a Nectar 10 location in the location database
         
        .DESCRIPTION
        Update a Nectar 10 location in the location database. This command can use the Google Geocode API to automatically populate the latitude/longitude for each location. You can register for an API key and save it as persistent environment variable called GoogleGeocode_API_Key on this machine. This command will prompt for the GeoCode API key and will save it in the appropriate location. Follow this link to get an API Key - https://developers.google.com/maps/documentation/geocoding/get-api-key. If this is not an option, then use the -SkipGeoLocate switch
 
        .PARAMETER SearchQuery
        A string to search for. Will search in Network, NetworkName, City, Street Address, Region etc.
         
        .PARAMETER Network
        The IP subnet of the network
         
        .PARAMETER NetworkMask
        The subnet mask of the network
         
        .PARAMETER ExtNetwork
        The IP subnet of the external/public network. Optional. Used to help differentiate calls from corporate locations that use common home subnets (192.168.x.x)
         
        .PARAMETER ExtNetworkMask
        The subnet mask of the external/public network. Optional. Used to help differentiate calls from corporate locations that use common home subnets (192.168.x.x)
         
        .PARAMETER NetworkName
        The name to give to the network
         
        .PARAMETER SiteName
        The name to give to the siteCode
         
        .PARAMETER SiteCode
        A site code to assign to the site
         
        .PARAMETER Region
        The name of the region. Typically is set to country name or whatever is appropriate for the company
         
        .PARAMETER StreetAddress
        The street address of the location
         
        .PARAMETER City
        The city of the location
         
        .PARAMETER State
        The state/province of the location
         
        .PARAMETER PostCode
        The postal/zip code of the location
         
        .PARAMETER Country
        The 2-letter ISO country code of the location
         
        .PARAMETER Description
        A description to apply to the location
         
        .PARAMETER IsWireless
        True or false if the network is strictly wireless
         
        .PARAMETER IsExternal
        True or false if the network is outside the corporate network
         
        .PARAMETER IsVPN
        True or false if the network is a VPN
         
        .PARAMETER Latitude
        The geographical latitude of the location. If not specified, will attempt automatic geolocation.
         
        .PARAMETER Longitude
        The geographical longitude of the location. If not specified, will attempt automatic geolocation.
         
        .PARAMETER SkipGeoLocate
        Don't attempt geolocation. Do this if you don't have a valid Google Maps API key.
         
        .PARAMETER Identity
        The numerical ID of the location to update. Can be obtained via Get-NectarLocation and pipelined to Set-NectarLocation
         
        .EXAMPLE
        Set-NectarLocation HeadOffice -Region WestUS
        Changes the region for HeadOffice to WestUS
 
        .EXAMPLE
        Get-NectarLocation | Set-NectarLocation
        Will go through each location and update the latitude/longitude. Useful if a Google Geocode API key was obtained after initial location loading
         
        .NOTES
        Version 1.11
    #>

    
    [Alias("snl")]
    Param (
        [Parameter(Mandatory=$False)]
        [string]$SearchQuery,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias("subnet")]
        [string]$Network,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias("networkRange", "subnetMask")]
        [ValidateRange(1,32)]
        [string]$NetworkMask,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$ExtNetwork,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateRange(0,32)]
        [string]$ExtNetworkMask,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateLength(1,99)]
        [Alias("Network Name")]
        [string]$NetworkName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$SiteName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias("Site Code")]
        [string]$SiteCode,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$Region,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias("address")]
        [string]$StreetAddress,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$City,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias("province")]
        [string]$State,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias("zipcode")]
        [string]$PostCode,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateLength(0,2)]
        [string]$Country,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$Description,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateSet("True","False","Yes","No",0,1, IgnoreCase=$True)]
        [Alias("isWirelessNetwork","Wireless(Yes/No)","Wireless","Wifi")]
        [string]$IsWireless,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateSet("True","False","Yes","No",0,1, IgnoreCase=$True)]
        [Alias("External(Yes/No)","External")]
        [string]$IsExternal,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateSet("True","False","Yes","No",0,1, IgnoreCase=$True)]
        [Alias("VPN","VPN(Yes/No)")]
        [string]$IsVPN,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateRange(-90,90)]
        [Alias("CoordinatesLatitude")]
        [double]$Latitude,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateRange(-180,180)]
        [Alias("CoordinatesLongitude")]
        [double]$Longitude,
        [parameter(Mandatory=$False)]
        [switch]$ForceGeoLocate,
        [parameter(Mandatory=$False)]
        [switch]$SkipGeoLocate,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias("id")]
        [String]$Identity
    )
    
    Begin {
        Connect-NectarCloud
    }        
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }

        Try {
            If ($SearchQuery) {
                $LocationInfo = Get-NectarLocation -SearchQuery $SearchQuery -Tenant $TenantName -ResultSize 1
                $Identity = $LocationInfo.id.ToString()
            }
            
            If (-not $Network) {$Network = $LocationInfo.network}
            If (-not $NetworkMask) {$NetworkMask = $LocationInfo.networkRange}
            If (-not $ExtNetwork) {$ExtNetwork = $LocationInfo.externalNetwork}
            If (-not $ExtNetworkMask) {$ExtNetworkMask = $LocationInfo.externalNetworkRange}
            If (-not $NetworkName) {$NetworkName = $LocationInfo.networkName}
            If (-not $SiteName) {$SiteName = $LocationInfo.siteName}
            If (-not $SiteCode) {$SiteCode = $LocationInfo.siteCode}
            If (-not $Region) {$Region = $LocationInfo.region}
            If (-not $StreetAddress) {$StreetAddress = $LocationInfo.streetAddress}
            If (-not $City) {$City = $LocationInfo.city}
            If (-not $State) {$State = $LocationInfo.state}
            If (-not $PostCode) {$PostCode = $LocationInfo.zipCode}
            If (-not $Country) {$Country = $LocationInfo.country}
            If (-not $Description) {$Description = $LocationInfo.description}
            If (-not $IsWireless) {$IsWireless = $LocationInfo.isWirelessNetwork}
            If (-not $IsExternal) {$IsExternal = $LocationInfo.isExternal}
            If (-not $IsVPN) {$IsVPN = $LocationInfo.vpn}
            If ($Latitude -eq $NULL -or $Latitude -eq 0) {$Latitude = $LocationInfo.latitude}
            If ($Longitude -eq $NULL -or $Longitude -eq 0) {$Longitude = $LocationInfo.longitude}
            If (-not $IsVPN) {$IsVPN = $LocationInfo.vpn}

            If ((($Latitude -eq $NULL -Or $Longitude -eq $NULL) -Or ($Latitude -eq 0 -And $Longitude -eq 0)) -Or $ForceGeoLocate -And !$SkipGeoLocate) {
                Write-Verbose "Lat/Long missing. Getting Lat/Long."
                $LatLong = Get-LatLong "$StreetAddress, $City, $State, $PostCode, $Region"
                $Latitude = $LatLong.Latitude
                $Longitude = $LatLong.Longitude
            }

            $URI = "https://$Global:NectarCloud/aapi/config/location/$Identity/?tenant=$TenantName"

            $Body = @{
                city = $City
                description = $Description
                id = $Identity
                isExternal = ParseBool $IsExternal
                isWirelessNetwork = ParseBool $IsWireless
                latitude = $Latitude
                longitude = $Longitude
                network = $Network
                networkRange = $NetworkMask
                externalNetwork = $ExtNetwork
                externalNetworkRange = $ExtNetworkMask
                networkName = $NetworkName
                region = $Region
                siteCode = $SiteCode
                siteName = $SiteName
                state = $State
                streetAddress = $StreetAddress
                country = $Country
                vpn = ParseBool $IsVPN
                zipCode = $PostCode
            }
            
            Write-Verbose $Body
            
            $JSONBody = $Body | ConvertTo-Json

            Try {
                Write-Verbose $JSONBody
                $JSON = Invoke-RestMethod -Method PUT -WebSession $Global:NectarSession -uri $URI -Body $JSONBody -ContentType 'application/json; charset=utf-8' 

            }
            Catch {
                If ($NetworkName) {
                    $IDText = $NetworkName
                }
                Else {
                    $IDText = "with ID $Identity"
                }
                
                Write-Error "Unable to apply changes for location $IDText."
                Get-JSONErrorStream -JSONResponse $_
            }
        }
        Catch {
            Write-Error $_
        }
    }
}


Function New-NectarLocation {
    <#
        .SYNOPSIS
        Creates a Nectar 10 location in the location database
         
        .DESCRIPTION
        Creates a Nectar 10 location in the location database. This command can use the Google Geocode API to automatically populate the latitude/longitude for each location. You can register for an API key and save it as persistent environment variable called GoogleGeocode_API_Key on this machine. This command will prompt for the GeoCode API key and will save it in the appropriate location. Follow this link to get an API Key - https://developers.google.com/maps/documentation/geocoding/get-api-key. If this is not an option, then use the -SkipGeoLocate switch
 
        .PARAMETER Network
        The IP subnet of the network
         
        .PARAMETER NetworkMask
        The subnet mask of the network
         
        .PARAMETER ExtNetwork
        The IP subnet of the external/public network. Optional. Used to help differentiate calls from corporate locations that use common home subnets (192.168.x.x)
         
        .PARAMETER ExtNetworkMask
        The subnet mask of the external/public network. Optional. Used to help differentiate calls from corporate locations that use common home subnets (192.168.x.x)
         
        .PARAMETER NetworkName
        The name to give to the network
         
        .PARAMETER SiteName
        The name to give to the site
         
        .PARAMETER SiteCode
        A site code to assign to the site
         
        .PARAMETER Region
        The name of the region. Typically is set to country name or whatever is appropriate for the company
         
        .PARAMETER StreetAddress
        The street address of the location
         
        .PARAMETER City
        The city of the location
         
        .PARAMETER State
        The state/province of the location
         
        .PARAMETER PostCode
        The postal/zip code of the location
         
        .PARAMETER Country
        The 2-letter ISO country code of the location
         
        .PARAMETER Description
        A description to apply to the location
         
        .PARAMETER IsWireless
        True or false if the network is strictly wireless
         
        .PARAMETER IsExternal
        True or false if the network is outside the corporate network
         
        .PARAMETER IsVPN
        True or false if the network is a VPN
         
        .PARAMETER SkipGeoLocate
        Don't attempt geolocation. Do this if you don't have a valid Google Maps API key.
         
        .PARAMETER Latitude
        The geographical latitude of the location. If not specified, will attempt automatic geolocation.
         
        .PARAMETER Longitude
        The geographical longitude of the location. If not specified, will attempt automatic geolocation.
 
        .PARAMETER TenantName
        The name of the Nectar 10 tenant. Used in multi-tenant configurations.
 
        .EXAMPLE
        New-NectarLocation -Network 10.14.3.0 -NetworkMask 24 -NetworkName Corp5thFloor -SiteName 'Head Office'
        Creates a new location using the minimum required information
         
        .EXAMPLE
        New-NectarLocation -Network 10.15.1.0 -NetworkMask 24 -ExtNetwork 79.23.155.71 -ExtNetworkMask 28 -NetworkName Corp3rdFloor -SiteName 'Head Office' -SiteCode HO3 -IsWireless True -IsVPN False -Region EastUS -StreetAddress '366 North Broadway' -City Jericho -State 'New York' -Country US -PostCode 11753 -Description 'Head office 3rd floor' -Latitude 40.7818283 -Longitude -73.5351438
        Creates a new location using all available fields
                 
        .EXAMPLE
        Import-Csv LocationData.csv | New-NectarLocation
        Imports a CSV file called LocationData.csv and creates new locations
         
        .EXAMPLE
        Import-Csv LocationData.csv | New-NectarLocation -SkipGeolocate
        Imports a CSV file called LocationData.csv and creates new locations but will not attempt geolocation
 
        .NOTES
        Version 1.1
    #>

    
    [Alias("nnl")]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [ValidatePattern('^([1-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])(\.([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])){3}$')]
        [Alias('subnet','searchquery')]
        [string]$Network,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [Alias('Network Range','networkRange', 'subnetMask')]
        [ValidateRange(1,32)]
        [string]$NetworkMask,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$ExtNetwork,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateRange(0,32)]
        [string]$ExtNetworkMask,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [ValidateLength(1,99)]
        [Alias('Network Name')]
        [string]$NetworkName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [Alias('Site Name')]
        [string]$SiteName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias('Site Code')]
        [string]$SiteCode,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$Region,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias('Street Address', 'address')]
        [string]$StreetAddress,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$City,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias('province')]
        [string]$State,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias('Zip Code', 'zipcode')]
        [string]$PostCode,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateLength(0,2)]
        [string]$Country,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$Description,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateSet('True','False','Yes','No',0,1, IgnoreCase=$True)]
        [Alias('isWirelessNetwork','Wireless(Yes/No)','Wireless(True/False)','Wireless','Wifi')]
        [string]$IsWireless,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateSet('True','False','Yes','No',0,1, IgnoreCase=$True)]
        [Alias('External(Yes/No)','External(True/False)','External','Internal/External')]
        [string]$IsExternal,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateSet('True','False','Yes','No',0,1, IgnoreCase=$True)]
        [Alias('VPN','VPN(Yes/No)','VPN(True/False)')]
        [string]$IsVPN,
        [parameter(Mandatory=$False)]
        [switch]$SkipGeoLocate,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateRange(-90,90)]
        [Alias('Coordinates Latitude','CoordinatesLatitude')]
        [double]$Latitude,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateRange(-180,180)]
        [Alias('Coordinates Longitude','CoordinatesLongitude')]
        [double]$Longitude,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName        
    )
    
    Begin {
        Connect-NectarCloud
    }        
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
        
        $URI = "https://$Global:NectarCloud/aapi/config/location?tenant=$TenantName"

        If (-not $Latitude -Or -not $Longitude -And !$SkipGeoLocate) {
            $LatLong = Get-LatLong "$StreetAddress, $City, $State, $PostCode, $Country"
            [double]$Latitude = $LatLong.Latitude
            [double]$Longitude = $LatLong.Longitude
        }

        $Body = @{
            city = $City
            description = $Description
            isExternal = ParseBool $IsExternal
            isWirelessNetwork = ParseBool $IsWireless
            latitude = $Latitude
            longitude = $Longitude
            network = $Network
            networkName = $NetworkName
            networkRange = $NetworkMask
            externalNetwork = $ExtNetworkName
            externalNetworkRange = $ExtNetworkMask
            region = $Region
            siteCode = $SiteCode
            siteName = $SiteName
            state = $State
            streetAddress = $StreetAddress
            country = $Country
            vpn = ParseBool $IsVPN
            zipCode = $PostCode
        }

        $JSONBody = $Body | ConvertTo-Json

        Try {
            Write-Verbose $JSONBody
            $JSON = Invoke-RestMethod -Method POST -WebSession $Global:NectarSession -uri $URI -Body $JSONBody -ContentType 'application/json; charset=utf-8'
        }
        Catch {
            Write-Error "Unable to create location $NetworkName with network $Network/$NetworkMask"
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}


Function Remove-NectarLocation {
    <#
        .SYNOPSIS
        Removes a Nectar 10 location from the location database
         
        .DESCRIPTION
        Removes a Nectar 10 location from the location database
 
        .PARAMETER SearchQuery
        The name of the location to remove. Can be a partial match. To return an exact match and to avoid ambiguity, enclose location name with ^ at the beginning and $ at the end.
 
        .PARAMETER TenantName
        The name of the Nectar 10 tenant. Used in multi-tenant configurations.
 
        .PARAMETER Identity
        The numerical ID of the location to remove. Can be obtained via Get-NectarLocation and pipelined to Remove-NectarLocation
         
        .NOTES
        Version 1.1
    #>

    
    [Alias("rnl")]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias("networkName")]
        [string]$SearchQuery, 
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias("id")]
        [string]$Identity
    )
    
    Begin {
        Connect-NectarCloud
    }        
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
            
        If ($SearchQuery -And !$Identity) {
            $LocationInfo = Get-NectarLocation -SearchQuery $SearchQuery -Tenant $TenantName -ResultSize 1 -ErrorVariable GetLocationError
            $Identity = $LocationInfo.id
            $NetworkName = $LocationInfo.networkName
        }
            
        If (!$GetLocationError) {
            $URI = "https://$Global:NectarCloud/aapi/config/location/$Identity/?tenant=$TenantName"
            
            Try {
                $JSON = Invoke-RestMethod -Method DELETE -WebSession $Global:NectarSession -uri $URI
                Write-Verbose "Successfully deleted $LocationName."
            }
            Catch {
                Write-Error "Unable to delete location $NetworkName. Ensure you typed the name of the location correctly."
                Get-JSONErrorStream -JSONResponse $_
            }
        }
    }
}



Function Import-NectarLocations {
    <#
        .SYNOPSIS
        Imports a CSV list of locations into Nectar 10
         
        .DESCRIPTION
        Import a CSV list of locations into Nectar 10. This will overwrite any existing locations with the same network ID. Useful for making wholesale changes without wiping and replacing everything.
        Assumes you are working from an export from the existing Nectar 10 location list.
 
        .PARAMETER Path
        The path to the CSV file to import into Nectar 10. The CSV file must use the standard column heading template used by Nectar 10 exports.
 
        .PARAMETER TenantName
        The name of the Nectar 10 tenant. Used in multi-tenant configurations.
         
        .PARAMETER SkipGeoLocate
        Don't attempt geolocation. Do this if you don't have a valid Google Maps API key or the lat/long is already included in the CSV.
         
        .NOTES
        Version 1.1
    #>

    
    Param (
        [Parameter(Mandatory=$True)]
        [string]$Path, 
        [Parameter(Mandatory=$False)]
        [string]$TenantName,
        [parameter(Mandatory=$False)]
        [switch]$SkipGeoLocate    
    )
    
    $LocTable = ((Get-Content -Path $Path -Raw) -replace '\(Yes/No\)','')
    $LocTable = $LocTable -replace '\"?Network\"?\,',"""SearchQuery""," 
    
    $LocationList = ConvertFrom-Csv $LocTable

    ForEach ($Location in $LocationList) {
        $LocationHashTable = @{}
        $Location.psobject.properties | ForEach { $LocationHashTable[$_.Name] = $_.Value }
        
        If ($TenantName) { $LocationHashTable += @{TenantName = $TenantName } }# Add the tenant name to the hashtable
        If ($SkipGeoLocate) { $LocationHashTable += @{SkipGeoLocate = $TRUE} }
        
        Try {
            Write-Host "Updating location with subnet $($Location.SearchQuery)"
            Write-Verbose $LocationHashTable
            Set-NectarLocation @LocationHashTable -ErrorAction:Stop
        }
        Catch {
            Write-Host "Location does not exist. Creating location $($Location.SearchQuery)"
            New-NectarLocation @LocationHashTable
        }
    }
}



Function Import-MSTeamsLocations {
    <#
        .SYNOPSIS
        Imports a CSV list of locations downloaded from Microsoft CQD into Nectar 10
         
        .DESCRIPTION
        Import a CSV list of locations downloaded from Microsoft CQD into Nectar 10. This will overwrite any existing locations with the same network ID. Useful for making wholesale changes without wiping and replacing everything.
 
        .PARAMETER Path
        The path to the CSV file to import into Nectar 10. The CSV file must be in the same format as downloaded from Microsoft CQD as per https://docs.microsoft.com/en-us/microsoftteams/cqd-upload-tenant-building-data
 
        .PARAMETER TenantName
        The name of the Nectar 10 tenant. Used in multi-tenant configurations.
         
        .PARAMETER SkipGeoLocate
        Don't attempt geolocation. Do this if you don't have a valid Google Maps API key.
         
        .NOTES
        Version 1.0
    #>

    
    Param (
        [Parameter(Mandatory=$True)]
        [string]$Path, 
        [Parameter(Mandatory=$False)]
        [string]$TenantName,
        [parameter(Mandatory=$False)]
        [switch]$SkipGeoLocate    
    )
    
    $Header = 'SearchQuery', 'NetworkName', 'NetworkMask', 'SiteName', 'OwnershipType', 'BuildingType', 'BuildingOfficeType', 'City', 'PostCode', 'Country', 'State', 'Region', 'IsExternal', 'ExpressRoute', 'IsVPN'
    $LocationList = Import-Csv $Path -Header $Header | Select-Object 'SearchQuery','NetworkMask','NetworkName','SiteName','City','PostCode','Country','State','Region','IsExternal','IsVPN'
    
    ForEach ($Location in $LocationList) {
        If ($Location.IsExternal -eq 0) { $Location.IsExternal = 1 } Else { $Location.IsExternal = 0 }
        If ($Location.IsVPN -eq 1) { $Location.IsVPN = 1 } Else { $Location.IsVPN = 0 }
    
        $LocationHashTable = @{}
        $Location.psobject.properties | ForEach { $LocationHashTable[$_.Name] = $_.Value }
        
        If ($TenantName) { $LocationHashTable += @{TenantName = $TenantName } }# Add the tenant name to the hashtable
        If ($SkipGeoLocate) { $LocationHashTable += @{SkipGeoLocate = $TRUE} }
                
        Try {
            Write-Host "Updating location with subnet $($Location.SearchQuery)"
            Write-Verbose $LocationHashTable
            Set-NectarLocation @LocationHashTable -ErrorAction:Stop
        }
        Catch {
            Write-Host "Location does not exist. Creating location $($Location.SearchQuery)"
            New-NectarLocation @LocationHashTable
        }
    }
}



#################################################################################################################################################
# #
# DID Number Location Management Functions #
# #
#################################################################################################################################################

Function Get-NectarNumberLocation {
    <#
        .SYNOPSIS
        Returns a list of Nectar 10 service locations used in the DID Management tool.
         
        .DESCRIPTION
        Returns a list of Nectar 10 service locations used in the DID Management tool.
 
        .PARAMETER LocationName
        The name of the service location to get information on. Can be a partial match. To return an exact match and to avoid ambiguity, enclose service location name with ^ at the beginning and $ at the end.
 
        .PARAMETER TenantName
        The name of the Nectar 10 tenant. Used in multi-tenant configurations.
         
        .PARAMETER ResultSize
        The number of results to return. Defaults to 1000.
         
        .EXAMPLE
        Get-NectarNumberLocation
        Returns the first 10 service locations
         
        .EXAMPLE
        Get-NectarNumberLocation -ResultSize 100
        Returns the first 100 service locations
 
        .EXAMPLE
        Get-NectarNumberLocation -LocationName Location2
        Returns up to 10 service locations that contains "location2" anywhere in the name. The search is not case-sensitive. This example would return Location2, Location20, Location214, MyLocation299 etc
 
        .EXAMPLE
        Get-NectarNumberLocation -LocationName ^Location2
        Returns up to 10 service locations that starts with "location2" in the name. The search is not case-sensitive. This example would return Location2, Location20, Location214 etc, but NOT MyLocation299
 
        .EXAMPLE
        Get-NectarNumberLocation -LocationName ^Location2$
        Returns a service location explicitly named "Location2". The search is not case-sensitive.
 
        .NOTES
        Version 1.1
    #>

    
    [Alias("gnnl")]
    Param (
        [Parameter(Mandatory=$False)]
        [string]$LocationName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,100000)]
        [int]$ResultSize = 1000
    )
    
    Begin {
        Connect-NectarCloud
    }        
    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
            
            $JSON = Invoke-RestMethod -Method GET -WebSession $Global:NectarSession -uri "https://$Global:NectarCloud/dapi/numbers/locations?pageNumber=1&tenant=$TenantName&pageSize=$ResultSize&q=$LocationName"

            If ($TenantName) {$JSON.elements | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty} # Add the tenant name to the output which helps pipelining
            $JSON.elements | Add-Member -TypeName 'Nectar.Number.LocationList'
            Return $JSON.elements
        }
        Catch {
            Write-Error "Service location not found. Ensure you typed the name of the service location correctly."
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}


Function Set-NectarNumberLocation {
    <#
        .SYNOPSIS
        Update a Nectar 10 service location used in the DID Management tool.
         
        .DESCRIPTION
        Update a Nectar 10 service location used in the DID Management tool.
 
        .PARAMETER LocationName
        The name of the service location to get information on. Can be a partial match. To return an exact match and to avoid ambiguity, enclose location name with ^ at the beginning and $ at the end.
         
        .PARAMETER NewLocationName
        Replace the existing service location name with this one.
         
        .PARAMETER ServiceID
        The service ID associated with the telephony provider for this service location
 
        .PARAMETER ServiceProvider
        The name of the service provider that provides telephony service to this service location
 
        .PARAMETER NetworkLocation
        The phyiscal location for this service location
 
        .PARAMETER Notes
        Can be used for any additional information
 
        .PARAMETER TenantName
        The name of the Nectar 10 tenant. Used in multi-tenant configurations.
         
        .EXAMPLE
        Set-NectarNumberLocation -LocationName Dallas -ServiceID 44FE98 -ServiceProvider Verizon -Notes "Head office"
        Returns the first 10 locations
         
        .NOTES
        Version 1.1
    #>

    
    [Alias("snnl")]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [Alias("name")]
        [string]$LocationName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$NewLocationName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        $ServiceID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias("provider")]
        $ServiceProvider,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias("location")]
        $NetworkLocation,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        $Notes,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias("id")]
        [String]$Identity
    )
    
    Begin {
        Connect-NectarCloud
    }        
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
        
        If ($LocationName -And !$Identity) {
            $LocationInfo = Get-NectarNumberLocation -LocationName $LocationName -Tenant $TenantName -ResultSize 1
            $Identity = $LocationInfo.id
        }    
        
        If ($LocationInfo.Count -gt 1) {
            Write-Error "Multiple number locations found that match $LocationName. Please refine your location name search query"
            Break
        }

        If ($NewLocationName) {$LocationName = $NewLocationName}
        If (-not $ServiceID) {$ServiceID = $LocationInfo.ServiceId}
        If (-not $ServiceProvider) {$ServiceProvider = $LocationInfo.provider}
        If (-not $NetworkLocation) {$NetworkLocation = $LocationInfo.location}
        If (-not $Notes) {$Notes = $LocationInfo.notes}

        $URI = "https://$Global:NectarCloud/dapi/numbers/location/$Identity/?tenant=$TenantName"

        $Body = @{
            name = $LocationName
            serviceId = $ServiceID
            provider = $ServiceProvider
            location = $NetworkLocation
            notes = $Notes
        }
        
        $JSONBody = $Body | ConvertTo-Json

        Try {
            $JSON = Invoke-RestMethod -Method PUT -WebSession $Global:NectarSession -uri $URI -Body $JSONBody -ContentType 'application/json; charset=utf-8'
            Write-Verbose $JSONBody
        }
        Catch {
            Write-Error "Unable to apply changes for location $LocationName."
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}


Function New-NectarNumberLocation {
    <#
        .SYNOPSIS
        Create a new Nectar Service Location for DID Management used in the DID Management tool.
         
        .DESCRIPTION
        Create a new Nectar Service Location for DID Management used in the DID Management tool.
 
        .PARAMETER LocationName
        The name of the new service location. Must be unique.
         
        .PARAMETER ServiceID
        The service ID for telephony services at the newservice location. Can be used as desired. Not required.
         
        .PARAMETER ServiceProvider
        The service provider for telephony services at the newservice location. Can be used as desired. Not required.
         
        .PARAMETER ServiceProvider
        The network location to associate with the newservice location. Can be used as desired. Not required.
 
        .PARAMETER Notes
        Any relevent notes about the service location. Can be used as desired. Not required.
 
        .PARAMETER TenantName
        The name of the Nectar 10 tenant. Used in multi-tenant configurations.
         
        .EXAMPLE
        New-NectarNumberLocation -LocationName Dallas -ServiceID 348FE22 -ServiceProvider Verizon -NetworkLocation Dallas -Notes "This is headquarters"
 
        .NOTES
        Version 1.1
    #>

    
    [Alias("nnnl")]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias("name")]
        [string]$LocationName, 
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$ServiceID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias("provider")]
        [string]$ServiceProvider,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias("location")]
        [string]$NetworkLocation,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$Notes,    
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
    }        
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
        
        $URI = "https://$Global:NectarCloud/dapi/numbers/location/?tenant=$TenantName"
        $Body = @{
            name = $LocationName
            serviceId = $ServiceID
            provider = $ServiceProvider
            location = $NetworkLocation
            notes = $Notes
        }
        
        $JSONBody = $Body | ConvertTo-Json

        Try {
            Invoke-RestMethod -Method POST -WebSession $Global:NectarSession -uri $URI -Body $JSONBody -ContentType 'application/json; charset=utf-8'
            Write-Verbose $JSONBody
        }
        Catch {
            Write-Error "Unable to create service location $LocationName. The service location may already exist."
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}


Function Remove-NectarNumberLocation {
    <#
        .SYNOPSIS
        Removes one or more service locations in the DID Management tool.
 
        .DESCRIPTION
        Removes one or more service locations in the DID Management tool.
         
        .PARAMETER LocationName
        The name of the number service location to remove.
     
        .PARAMETER Identity
        The numerical ID of the number service location. Can be obtained via Get-NectarNumberLocation and pipelined to Remove-NectarNumberLocation
 
        .PARAMETER TenantName
        The name of the Nectar 10 tenant. Used in multi-tenant configurations.
         
        .EXAMPLE
        Remove-NectarNumberLocation Tokyo
        Removes the Toyota location. The command will fail if the location has number ranges assigned.
         
        .NOTES
        Version 1.1
    #>

    
    [Alias("rnnl")]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$LocationName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,        
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias("id")]
        [string]$Identity
    )
    
    Begin {
        Connect-NectarCloud
    }        
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
        
        If ($LocationName -And !$Identity) {
            $Identity = (Get-NectarNumberLocation -LocationName $LocationName -Tenant $TenantName -ResultSize 1 -ErrorVariable GetLocationError).ID
        }
        
        If ($Identity.Count -gt 1) {
            Write-Error "Multiple number locations found that match $LocationName. Please refine your location name search query"
            Break
        }
            
        If (!$GetLocationError) {
            $URI = "https://$Global:NectarCloud/dapi/numbers/location/$Identity/?tenant=$TenantName"
            
            Try {
                $JSON = Invoke-RestMethod -Method DELETE -WebSession $Global:NectarSession -uri $URI
                Write-Verbose "Successfully deleted $LocationName."
            }
            Catch {
                Write-Error "Unable to delete service location $LocationName. Ensure you typed the name of the service location correctly and that the service location has no assigned ranges."
                Get-JSONErrorStream -JSONResponse $_
            }
        }
    }
}




#################################################################################################################################################
# #
# DID Number Range Management Functions #
# #
############################################################################################################################################################################################################### DID Management Functions - Number Range ##############################################################

Function Get-NectarNumberRange {
    <#
        .SYNOPSIS
        Returns a list of Nectar 10 number ranges in the DID Management tool
 
        .DESCRIPTION
        Returns a list of Nectar 10 ranges in the DID Management tool
         
        .PARAMETER RangeName
        The name of the number range to get information on. Can be a partial match. To return an exact match and to avoid ambiguity, enclose range name with ^ at the beginning and $ at the end.
     
        .PARAMETER LocationName
        The name of the location to get information on. Will be an exact match.
 
        .PARAMETER TenantName
        The name of the Nectar 10 tenant. Used in multi-tenant configurations.
         
        .PARAMETER ResultSize
        The number of results to return. Defaults to 1000.
         
        .EXAMPLE
        Get-NectarNumberRange
        Returns the first 10 number ranges
         
        .EXAMPLE
        Get-NectarNumberRange -ResultSize 100
        Returns the first 100 number ranges
 
        .EXAMPLE
        Get-NectarNumberRange -LocationName Tokyo
        Returns the first 10 number ranges at the Tokyo location
 
        .EXAMPLE
        Get-NectarNumberRange -RangeName Range2
        Returns up to 10 ranges that contains "range2" anywhere in the name. The search is not case-sensitive. This example would return Range2, Range20, Range214, MyRange299 etc
 
        .EXAMPLE
        Get-NectarNumberRange -RangeName ^Range2
        Returns up to 10 ranges that starts with "range2" in the name. The search is not case-sensitive. This example would return Range2, Range20, Range214 etc, but NOT MyRange299.
 
        .EXAMPLE
        Get-NectarNumberRange -RangeName ^Range2$
        Returns any range explicitly named "Range2". The search is not case-sensitive. This example would return Range2 only. If there are multiple ranges with the name Range2, all will be returned.
 
        .EXAMPLE
        Get-NectarNumberRange -RangeName ^Range2$ -LocationName Tokyo
        Returns a range explicitly named "Range2" in the Tokyo location. The search is not case-sensitive.
 
        .NOTES
        Version 1.1
    #>

    
    [Alias("gnnr")]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$RangeName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$LocationName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,100000)]
        [int]$ResultSize = 1000
    )
    
    Begin {
        Connect-NectarCloud
    }        
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
        
        If ($LocationName) {    
            $LocationID = (Get-NectarNumberLocation -LocationName "$LocationName" -Tenant $TenantName -ErrorVariable LocError).ID 
        }
        
        If ($LocError) { Break}
        
        Try {
            $JSON = Invoke-RestMethod -Method GET -WebSession $Global:NectarSession -uri "https://$Global:NectarCloud/dapi/numbers/ranges?pageNumber=1&tenant=$TenantName&pageSize=$ResultSize&serviceLocationId=$LocationID&q=$RangeName"
            If ($TenantName) {$JSON.elements | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty} # Add the tenant name to the output which helps pipelining
            $JSON.elements | Add-Member -TypeName 'Nectar.Number.RangeList'
            Return $JSON.elements
        }
        Catch {
            Write-Error "An error occurred."
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}


Function Set-NectarNumberRange {
    <#
        .SYNOPSIS
        Make changes to a Nectar range for DID Management
         
        .DESCRIPTION
        Make changes to a Nectar range for DID Management
 
        .PARAMETER RangeName
        The name of the range. Must be unique.
         
        .PARAMETER RangeType
        The type of range. Can be either STANDARD (for DID ranges) or EXTENSION (for extension-based ranges).
         
        .PARAMETER FirstNumber
        The first number in a STANDARD range. Must be numeric, but can start with +.
         
        .PARAMETER LastNumber
        The last number in a STANDARD range. Must be numeric, but can start with +. Must be larger than FirstNumber, and must have the same number of digits.
         
        .PARAMETER BaseNumber
        The base DID for an EXTENSION range. Must be numeric, but can start with +.
         
        .PARAMETER ExtStart
        The first extension number in an EXTENSION range. Must be numeric.
         
        .PARAMETER ExtEnd
        The last extension number in an EXTENSION range. Must be numeric. Must be larger than ExtStart, and must have the same number of digits.
 
        .PARAMETER RangeSize
        The number of phone numbers/extensions in a range. Can be used instead of LastNumber/ExtEnd.
         
        .PARAMETER HoldDays
        The number of days to hold a newly-freed number before returning it to the pool of available numbers.
         
        .PARAMETER LocationName
        The service location to assign the range to.
 
        .PARAMETER TenantName
        The name of the Nectar 10 tenant. Used in multi-tenant configurations.
         
        .EXAMPLE
        Set-NectarNumberRange -RangeName DIDRange1 -RangeType STANDARD -FirstNumber +15552223333 -LastNumber +15552224444 -LocationName Dallas
        Edits a DID range for numbers that fall in the range of +15552223333 to +15552224444
         
        .NOTES
        Version 1.1
    #>

    
    [Alias("snnr")]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias("name")]
        [string]$RangeName, 
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateSet("STANDARD","EXTENSION", IgnoreCase=$True)]
        [Alias("type")]
        [string]$RangeType,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
# [ValidatePattern("^(\+|%2B)?\d+$")]
        [string]$FirstNumber,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
# [ValidatePattern("^(\+|%2B)?\d+$")]
        [string]$LastNumber,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
# [ValidatePattern("^(\+|%2B)?\d+$")]
        [string]$BaseNumber,        
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
# [ValidatePattern("^\d+$")]
        [string]$ExtStart,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
# [ValidatePattern("^\d+$")]
        [string]$ExtEnd,            
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [int]$RangeSize,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [int]$HoldDays = 0,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [Alias("serviceLocationId")]
        [string]$LocationName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias("id")]
        [int]$Identity
    )
    
    Begin {
        Connect-NectarCloud
    }        
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
        
        If ($RangeName -And !$Identity) {
            $RangeInfo = Get-NectarNumberRange -RangeName $RangeName -Tenant $TenantName -ResultSize 1
            $Identity = $RangeInfo.id
            
            If ($NewRangeName) {$RangeName = $NewRangeName}
            If (-not $FirstNumber) {$FirstNumber = $RangeInfo.firstNumber}
            If (-not $LastNumber) {$LastNumber = $RangeInfo.lastNumber}
            If (-not $RangeType) {$RangeType = $RangeInfo.type}
            If (-not $HoldDays) {$HoldDays = $RangeInfo.holdDays}
            If (-not $BaseNumber) {$BaseNumber = $RangeInfo.baseNumber}
            If (-not $ExtStart) {$ExtStart = $RangeInfo.extStart}
            If (-not $ExtEnd) {$ExtEnd = $RangeInfo.extEnd}
            # If (-not $LocationName) {$LocationID = $RangeInfo.serviceLocationId}
        }
            
        $LocationID = (Get-NectarNumberLocation -LocationName "$LocationName" -Tenant $TenantName -ErrorVariable LocError).ID
        
        $URI = "https://$Global:NectarCloud/dapi/numbers/range/$Identity/?tenant=$TenantName"

        $Body = @{
            name = $RangeName
            type = $RangeType
            holdDays = $HoldDays
            serviceLocationId = $LocationID
        }

        If ($FirstNumber) { $Body.Add('firstNumber', $FirstNumber) }
        If ($LastNumber) { $Body.Add('lastNumber', $LastNumber) }
        If ($BaseNumber) { $Body.Add('baseNumber', $BaseNumber) }
        If ($ExtStart) { $Body.Add('extStart', $ExtStart) }
        If ($ExtEnd) { $Body.Add('extEnd', $ExtEnd) }
        If ($RangeSize) { $Body.Add('rangeSize', $RangeSize) }
        
        $JSONBody = $Body | ConvertTo-Json

        Try {
            $JSON = Invoke-RestMethod -Method PUT -WebSession $Global:NectarSession -uri $URI -Body $JSONBody -ContentType 'application/json; charset=utf-8'
            Write-Verbose $JSONBody
        }
        Catch {
            Write-Error "Unable to apply changes for range $RangeName."
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}


Function New-NectarNumberRange {
    <#
        .SYNOPSIS
        Create a new Nectar range for DID Management
         
        .DESCRIPTION
        Create a new Nectar range for DID Management
 
        .PARAMETER RangeName
        The name of the new range. Must be unique.
         
        .PARAMETER RangeType
        The type of range. Can be either STANDARD (for DID ranges) or EXTENSION (for extension-based ranges).
         
        .PARAMETER FirstNumber
        The first number in a STANDARD range. Must be numeric, but can start with +.
         
        .PARAMETER LastNumber
        The last number in a STANDARD range. Must be numeric, but can start with +. Must be larger than FirstNumber, and must have the same number of digits.
         
        .PARAMETER BaseNumber
        The base DID for an EXTENSION range. Must be numeric, but can start with +.
         
        .PARAMETER ExtStart
        The first extension number in an EXTENSION range. Must be numeric.
         
        .PARAMETER ExtEnd
        The last extension number in an EXTENSION range. Must be numeric. Must be larger than ExtStart, and must have the same number of digits.
 
        .PARAMETER RangeSize
        The number of phone numbers/extensions in a range. Can be used instead of LastNumber/ExtEnd.
         
        .PARAMETER HoldDays
        The number of days to hold a newly-freed number before returning it to the pool of available numbers.
         
        .PARAMETER LocationName
        The location to assign the range to.
 
        .PARAMETER TenantName
        The name of the Nectar 10 tenant. Used in multi-tenant configurations.
                 
        .EXAMPLE
        New-NectarNumberRange -RangeName DIDRange1 -RangeType STANDARD -FirstNumber +15552223333 -LastNumber +15552224444 -LocationName Dallas
        Creates a DID range for numbers that fall in the range of +15552223333 to +15552224444
 
        .EXAMPLE
        New-NectarNumberRange -RangeName DIDRange1 -RangeType STANDARD -FirstNumber +15552223000 -RangeSize 1000 -LocationName Dallas
        Creates a DID range for numbers that fall in the range of +15552223000 to +15552223999
         
        .EXAMPLE
        New-NectarNumberRange -RangeName ExtRange1 -RangeType EXTENSION -BaseNumber +15552223000 -ExtStart 2000 -ExtEnd 2999 -LocationName Dallas
        Creates an extension range for numbers that fall in the range of +15552223000 x2000 to x2999
         
        .NOTES
        Version 1.2
    #>

    
    [Alias("nnnr")]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [Alias("name")]
        [string]$RangeName, 
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [ValidateSet("STANDARD","EXTENSION", IgnoreCase=$True)]
        [string]$RangeType,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidatePattern("^(\+|%2B)?\d+$")]
        [string]$FirstNumber,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidatePattern("^(\+|%2B)?\d+$")]
        [string]$LastNumber,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidatePattern("^(\+|%2B)?\d+$")]
        [string]$BaseNumber,        
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidatePattern("^\d+$")]
        [string]$ExtStart,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidatePattern("^\d+$")]
        [string]$ExtEnd,            
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [int]$RangeSize,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [int]$HoldDays,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [string]$LocationName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias("id")]
        [int]$Identity
    )
    
    Begin {
        Connect-NectarCloud
    }        
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
        
        $LocationID = (Get-NectarNumberLocation -LocationName $LocationName -Tenant $TenantName -ErrorVariable NumLocError).ID
        
        If ($LocationID.Count -gt 1) {
            Write-Error "Multiple locations found that match $LocationName. Please refine your location name search query"
            Break
        }
    
        $URI = "https://$Global:NectarCloud/dapi/numbers/range?tenant=$TenantName"
        $Body = @{
            name = $RangeName
            type = $RangeType
            holdDays = $HoldDays
            serviceLocationId = $LocationID
        }

        If ($FirstNumber) { $Body.Add('firstNumber', $FirstNumber) }
        If ($LastNumber) { $Body.Add('lastNumber', $LastNumber) }
        If ($BaseNumber) { $Body.Add('baseNumber', $BaseNumber) }
        If ($ExtStart) { $Body.Add('extStart', $ExtStart) }
        If ($ExtEnd) { $Body.Add('extEnd', $ExtEnd) }
        If ($RangeSize) { $Body.Add('rangeSize', $RangeSize) }
        
        $JSONBody = $Body | ConvertTo-Json

        Try {
            Invoke-RestMethod -Method POST -WebSession $Global:NectarSession -uri $URI -Body $JSONBody -ContentType 'application/json; charset=utf-8'
            Write-Verbose $JSONBody
        }
        Catch {
            Write-Error "Unable to create range $RangeName."
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}


Function Remove-NectarNumberRange {
    <#
        .SYNOPSIS
        Removes one or more ranges from a service location in the DID Management tool.
 
        .DESCRIPTION
        Removes one or more ranges from a service location in the DID Management tool.
         
        .PARAMETER RangeName
        The name of the number range to remove.
 
        .PARAMETER TenantName
        The name of the Nectar 10 tenant. Used in multi-tenant configurations.
                 
        .PARAMETER Identity
        The numerical ID of the number range. Can be obtained via Get-NectarNumberRange and pipelined to Remove-NectarNumberRange
         
        .EXAMPLE
        Remove-NectarNumberRange Range1
        Removes the range Range1
         
        .NOTES
        Version 1.1
    #>

    
    [Alias("rnnr")]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$RangeName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias("id")]
        [String]$Identity
    )

    Begin {
        Connect-NectarCloud
    }    
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
        
        If ($RangeName -And !$Identity) {
            $Identity = (Get-NectarNumberRange -RangeName $RangeName -Tenant $TenantName -ErrorVariable GetRangeError).ID
        }
        
        If ($Identity.Count -gt 1) {
            Write-Error "Multiple ranges found that match $RangeName. Please refine your range name search query"
            Break
        }
            
        If (!$GetRangeError) {
            $URI = "https://$Global:NectarCloud/dapi/numbers/range/$Identity/?tenant=$TenantName"
            
            Try {
                $JSON = Invoke-RestMethod -Method DELETE -WebSession $Global:NectarSession -uri $URI
                Write-Verbose "Successfully deleted $RangeName number range."
            }
            Catch {
                Write-Error "Unable to delete $RangeName number range. Ensure you typed the name of the range correctly."
                Get-JSONErrorStream -JSONResponse $_
            }
        }
    }
}




#################################################################################################################################################
# #
# DID Number Management Functions #
# #
#################################################################################################################################################

Function Get-NectarNumber {
    <#
        .SYNOPSIS
        Returns a list of Nectar 10 numbers from the DID Management tool
 
        .DESCRIPTION
        Returns a list of Nectar 10 numbers from the DID Management tool
         
        .PARAMETER PhoneNumber
        The phone number to return information about. Can be a partial match. To return an exact match and to avoid ambiguity, enclose number with ^ at the beginning and $ at the end.
     
        .PARAMETER LocationName
        The name of the location to get number information about. Will be an exact match.
 
        .PARAMETER RangeName
        The name of the range to get number information about. Will be an exact match.
 
        .PARAMETER NumberState
        Returns information about numbers that are either USED, UNUSED or RESERVED
 
        .PARAMETER TenantName
        The name of the Nectar 10 tenant. Used in multi-tenant configurations.
         
        .PARAMETER ResultSize
        The number of results to return. Defaults to 1000.
         
        .EXAMPLE
        Get-NectarNumber
        Returns the first 10 numbers
         
        .EXAMPLE
        Get-NectarNumber -ResultSize 100
        Returns the first 100 numbers
 
        .EXAMPLE
        Get-NectarNumber -LocationName Tokyo
        Returns the first 10 numbers at the Tokyo location
 
        .EXAMPLE
        Get-NectarNumber -RangeName Range2
        Returns up to 10 numbers from a number range called Range2.
 
        .EXAMPLE
        Get-NectarNumber -RangeName Range2 -NumberState UNUSED -ResultSize 100
        Returns up to 100 unused numbers in the Range2 range.
 
        .NOTES
        Version 1.1
    #>

    
    [Alias("gnn")]    
    Param (
        [Parameter(Mandatory=$False)]
        [ValidatePattern("^(\+|%2B)?\d+$")]
        [string]$PhoneNumber,
        [Parameter(Mandatory=$False)]
        [string]$LocationName, 
        [Parameter(Mandatory=$False)]
        [string]$RangeName,
        [Parameter(Mandatory=$False)]
        [ValidateSet("USED","UNUSED","RESERVED", IgnoreCase=$True)]
        [string]$NumberState,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,100000)]
        [int]$PageSize = 10000,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,100000)]
        [int]$ResultSize
    )
    
    Begin {
        Connect-NectarCloud
    }        
    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
            
            If ($LocationName) {
                $LocationID = (Get-NectarNumberLocation -LocationName ^$LocationName$ -Tenant $TenantName -ResultSize 1 -ErrorVariable NectarError).ID 
            }
            
            If ($RangeName) {
                $RangeID = (Get-NectarNumberRange -RangeName $RangeName -LocationName $LocationName -Tenant $TenantName -ResultSize 1 -ErrorVariable +NectarError).ID
            }
            
            If ($PhoneNumber) {
                # Replace + with %2B if present
                $PhoneNumber = $PhoneNumber.Replace("+", "%2B")
            }
            
            $URI = "https://$Global:NectarCloud/dapi/numbers/"
            
            $Params = @{
                'orderByField' = 'number'
                'orderDirection' = 'asc'
            }
            
            If ($ResultSize) { 
                $Params.Add('pageSize', $ResultSize) 
            }
            Else { 
                $Params.Add('pageSize', $PageSize)
            }

            If ($LocationID) { $Params.Add('serviceLocationId', $LocationID) }
            If ($RangeID) { $Params.Add('numbersRangeId', $RangeID) }
            If ($NumberState) { $Params.Add('states', $NumberState) }
            If ($PhoneNumber) { $Params.Add('q', $PhoneNumber) }
            If ($TenantName) { $Params.Add('Tenant', $TenantName) }            
            
            If (!$NectarError) {
                $JSON = Invoke-RestMethod -Method GET -WebSession $Global:NectarSession -uri $URI -Body $Params
                If ($TenantName) { $JSON.elements | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty } # Add the tenant name to the output which helps pipelining
                $JSON.elements | Add-Member -TypeName 'Nectar.Number.List'
                $JSON.elements
                
                $TotalPages = $JSON.totalPages
            
                If ($TotalPages -gt 1 -and !($ResultSize)) {
                    $PageNum = 2
                    Write-Verbose "Page size: $PageSize"
                    While ($PageNum -le $TotalPages) {
                        Write-Verbose "Working on page $PageNum of $TotalPages"
                        $PagedURI = $URI + "?pageNumber=$PageNum"
                        $JSON = Invoke-RestMethod -Method GET -WebSession $Global:NectarSession -uri $PagedURI -Body $Params
                        If ($TenantName) { $JSON.elements | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty }
                        $JSON.elements | Add-Member -TypeName 'Nectar.Number.List'
                        $JSON.elements
                        $PageNum++
                    }
                }
            }
        }
        Catch {
            Write-Error "Unable to retrieve number information"
            Get-JSONErrorStream -JSONResponse $_        
        }
    }
}


Function Set-NectarNumber {
    <#
        .SYNOPSIS
        Makes changes to one or more phone numbers.
         
        .DESCRIPTION
        Makes changes to one or more phone numbers.
 
        .PARAMETER PhoneNumber
        A phone number to make changes to. Must be an exact match.
         
        .PARAMETER NumberState
        Change the state of a phone number to either UNUSED or RESERVED. A number marked USED cannot be modified.
         
        .PARAMETER Comment
        A comment to add to a reserved phone number.
 
        .PARAMETER TenantName
        The name of the Nectar 10 tenant. Used in multi-tenant configurations.
         
        .EXAMPLE
        Set-NectarNumber +12223334444 -NumberState RESERVED
        Reserves the number +12223334444
         
        .NOTES
        Version 1.1
    #>


    [Alias("snn")]
    Param (
        [Parameter(ValueFromPipelineByPropertyName,Mandatory=$False)]
        [ValidatePattern("^(\+|%2B)?\d+$")]
        [string]$PhoneNumber,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateSet("UNUSED","RESERVED", IgnoreCase=$True)]
        [string]$NumberState,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateLength(0,254)]
        [string]$Comment,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [parameter(ValueFromPipelineByPropertyName)]
        [Alias("id")]
        [String]$Identity
    )
    
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
        
        If ($PhoneNumber -And !$Identity) {
            $Identity = (Get-NectarNumber -PhoneNumber $PhoneNumber -Tenant $TenantName -ResultSize 1 -ErrorVariable PhoneNumError).ID
        }
        
        If (!$PhoneNumError) {
            $URI = "https://$Global:NectarCloud/dapi/numbers/$Identity/state?state=$NumberState&tenant=$TenantName"
        
            If (($Comment) -And ($NumberState -eq "RESERVED")) {
                # Convert special characters to URI-compatible versions
                #$Comment = [uri]::EscapeDataString($Comment)
                $URI += "&comment=$Comment"
            }
            
            Try {
                $JSON = Invoke-RestMethod -Method PUT -WebSession $Global:NectarSession -uri $URI
                Write-Verbose "Successfully applied changes to $PhoneNumber."
            }
            Catch {
                Write-Error "Unable to apply changes for phone number $PhoneNumber. The number may already be in the desired state."
                Get-JSONErrorStream -JSONResponse $_
            }
        }
    }
}


Function Get-NectarUnallocatedNumber {
    <#
        .SYNOPSIS
        Returns the next available number in a given location/range.
 
        .DESCRIPTION
        Returns the next available number in a given location/range.
         
        .PARAMETER LocationName
        The service location to return a number for
         
        .PARAMETER RangeName
        The range to return a number for
 
        .PARAMETER TenantName
        The name of the Nectar 10 tenant. Used in multi-tenant configurations.
         
        .EXAMPLE
        Get-NectarUnallocatedNumber -RangeName Jericho
        Returns the next available number in the Jericho range.
             
        .NOTES
        Version 1.1
    #>

    
    [Alias("gnun")]
    Param (
        [Parameter(Mandatory=$False)]
        [string]$LocationName, 
        [Parameter(Mandatory=$False)]
        [string]$RangeName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    # Use globally set tenant name, if one was set and not explicitly included in the command
    If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
    
    $NextFreeNum = Get-NectarNumber -LocationName $LocationName -RangeName $RangeName -NumberState UNUSED -Tenant $TenantName -ResultSize 1 

    If ($NextFreeNum) {
        Return $NextFreeNum
    }
    Else {
        Write-Error "No available phone number found."
    }
}




#################################################################################################################################################
# #
# Supported Device Functions #
# #
#################################################################################################################################################

Function Get-NectarSupportedDevice {
    <#
        .SYNOPSIS
        Get information about 1 or more Nectar 10 supported devices.
         
        .DESCRIPTION
        Get information about 1 or more Nectar 10 supported devices.
 
        .PARAMETER SearchQuery
        A full or partial match of the device's name
 
        .PARAMETER TenantName
        The name of the Nectar 10 tenant. Used in multi-tenant configurations.
         
        .PARAMETER ResultSize
        The number of results to return. Defaults to 10000.
         
        .EXAMPLE
        Get-NectarSupportedDevice -SearchQuery Realtek
         
        .NOTES
        Version 1.1
    #>

    
    [Alias("gnd")]
    Param (
        [Parameter(Mandatory=$False)]
        [string]$SearchQuery,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,100000)]
        [int]$ResultSize = 10000
    )    
    
    Begin {
        Connect-NectarCloud
    }        
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
        
        $URI = "https://$Global:NectarCloud/aapi/supported/devices?q=$SearchQuery&tenant=$TenantName&pageSize=$ResultSize"
        Try {
            $JSON = Invoke-RestMethod -Method GET -WebSession $Global:NectarSession -uri $URI    
            If ($TenantName) {$JSON.elements | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty} # Add the tenant name to the output which helps pipelining
            $JSON.elements | Add-Member -TypeName 'Nectar.DeviceList'
            Return $JSON.elements
        }
        Catch {
            Write-Error "Unable to get device details."
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}


Function Set-NectarSupportedDevice {
    <#
        .SYNOPSIS
        Update 1 or more Nectar 10 supported device.
         
        .DESCRIPTION
        Update 1 or more Nectar 10 supported device.
 
        .PARAMETER DeviceName
        The name of the supported device
 
        .PARAMETER TenantName
        The name of the Nectar 10 tenant. Used in multi-tenant configurations.
         
        .PARAMETER Identity
        The numerical identity of the supported device
         
        .EXAMPLE
        Set-NectarSupportedDevice Identity 233 -Supported $FALSE
         
        .EXAMPLE
        Get-NectarSupportedDevice -SearchQuery realtek | Set-NectarSupportedDevice -Supported $FALSE
        Sets all devices with 'Realtek' in the name to Unsupported
         
        .NOTES
        Version 1.1
    #>

    
    [Alias("snsd")]
    Param (
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$DeviceName,
        [Alias("deviceSupported")]
        [bool]$Supported,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias("deviceKey")]
        [string]$Identity
    )
    
    Begin {
        Connect-NectarCloud
    }        
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
        
        If ($DeviceName -and !$Identity) {
            $DeviceInfo = Get-NectarSupportedDevice -SearchQuery $DeviceName -Tenant $TenantName -ResultSize 1
            
            $DeviceName = $DeviceInfo.DeviceName
            If ($Supported -eq $NULL) {$Supported = $DeviceInfo.deviceSupported}
            $Identity = $DeviceInfo.deviceKey
        }
            
        $URI = "https://$Global:NectarCloud/aapi/supported/device/$Identity/?tenant=$TenantName"

        $Body = @{
            deviceKey = $Identity
            deviceSupported = $Supported
        }
        
        $JSONBody = $Body | ConvertTo-Json

        Try {
            $JSON = Invoke-RestMethod -Method PUT -WebSession $Global:NectarSession -uri $URI -Body $JSONBody -ContentType 'application/json; charset=utf-8'
            Write-Verbose $JSONBody
        }
        Catch {
            If ($DeviceName) {
                $IDText = $DeviceName
            }
            Else {
                $IDText = "with ID $Identity"
            }
            
            Write-Error "Unable to apply changes for device $IDText."
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



#################################################################################################################################################
# #
# Supported Client Functions #
# #
#################################################################################################################################################

Function Get-NectarSupportedClient {
    <#
        .SYNOPSIS
        Get information about 1 or more Nectar 10 supported client versions.
         
        .DESCRIPTION
        Get information about 1 or more Nectar 10 supported client versions.
 
        .PARAMETER SearchQuery
        A full or partial match of the client versions's name
 
        .PARAMETER TenantName
        The name of the Nectar 10 tenant. Used in multi-tenant configurations.
         
        .PARAMETER ResultSize
        The number of results to return. Defaults to 1000.
         
        .EXAMPLE
        Get-NectarSupportedClient -SearchQuery Skype
         
        .NOTES
        Version 1.1
    #>

    
    [Alias("gnsc")]
    Param (
        [Parameter(Mandatory=$False)]
        [string]$SearchQuery,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,100000)]
        [int]$ResultSize = 10000
    )    
    
    Begin {
        Connect-NectarCloud
    }        
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
        
        $URI = "https://$Global:NectarCloud/aapi/supported/client/versions?q=$SearchQuery&tenant=$TenantName&pageSize=$ResultSize"
        Try {
            $JSON = Invoke-RestMethod -Method GET -WebSession $Global:NectarSession -uri $URI    
            If ($TenantName) {$JSON.elements | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty} # Add the tenant name to the output which helps pipelining
            $JSON.elements | Add-Member -TypeName 'Nectar.ClientList'
            Return $JSON.elements
        }
        Catch {
            Write-Error "Unable to get client version details."
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}


Function Set-NectarSupportedClient {
    <#
        .SYNOPSIS
        Update 1 or more Nectar 10 supported client versions.
         
        .DESCRIPTION
        Update 1 or more Nectar 10 supported client versions.
 
        .PARAMETER TenantName
        The name of the Nectar 10 tenant. Used in multi-tenant configurations.
 
        .PARAMETER Identity
        The numerical identity of the supported client version.
         
        .EXAMPLE
        Set-NectarSupportedClient Identity 233 -Supported $FALSE
        Sets the device with identity 233 to unsupported
     
        .NOTES
        Version 1.1
    #>

    
    [Alias("snsc")]
    Param (
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias("version")]
        [string]$ClientVersion,
        [Alias("clientVersionSupported")]
        [bool]$Supported,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias("versionId")]
        [int]$Identity,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$Platform
    )
    
    Begin {
        Connect-NectarCloud
    }        
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
        
        If ($ClientVersion -and !$Identity) {
            $ClientInfo = Get-NectarSupportedClient -SearchQuery $ClientVersion -Tenant $TenantName -ResultSize 1
            
            $ClientVersion = $ClientInfo.version
            If ($Supported -eq $NULL) {$Supported = $ClientInfo.clientVersionSupported}
            $Identity = $ClientInfo.versionId
            $Platform = $ClientInfo.platform
        }
            
        $URI = "https://$Global:NectarCloud/aapi/supported/client/version?versionName=$ClientVersion&tenant=$TenantName"

        $Body = @{
             clientVersionSupported = $Supported
             platform = $Platform
             versionId = $Identity
        }
        
        $JSONBody = $Body | ConvertTo-Json

        Try {
            $JSON = Invoke-RestMethod -Method PUT -WebSession $Global:NectarSession -uri $URI -Body $JSONBody -ContentType 'application/json; charset=utf-8'
            Write-Verbose $JSONBody
        }
        Catch {
            If ($ClientVersion) {
                $IDText = $ClientVersion
            }
            Else {
                $IDText = "with ID $Identity"
            }
            
            Write-Error "Unable to apply changes for client version $IDText."
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}


#################################################################################################################################################
# #
# User Functions #
# #
#################################################################################################################################################

Function Get-NectarUser {
    <#
        .SYNOPSIS
        Get information about 1 or more users via Nectar 10.
         
        .DESCRIPTION
        Get information about 1 or more users via Nectar 10.
 
        .PARAMETER SearchQuery
        A full or partial match of the user name. Will do an 'includes' type search by default. So searching for user@domain.com would return Auser@domain.com, Buser@domain.com etc.
        For a specific match, enclose the name in square brackets IE: [user@domain.com]
 
        .PARAMETER TenantName
        The name of the Nectar 10 tenant. Used in multi-tenant configurations.
         
        .PARAMETER ResultSize
        The number of results to return. Defaults to 1000.
         
        .EXAMPLE
        Get-NectarUser -SearchQuery tferguson
         
        .NOTES
        Version 1.1
    #>

    
    [Alias("gnu")]
    Param (
        [Parameter(Mandatory=$True)]
        [string]$SearchQuery,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,100000)]
        [int]$ResultSize = 100
    )    
    
    Begin {
        Connect-NectarCloud
    }        
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
        
        $SearchQuery = [System.Web.HttpUtility]::UrlEncode($SearchQuery)
        
        $URI = "https://$Global:NectarCloud/dapi/info/session/users?q=$SearchQuery&pageSize=$ResultSize&tenant=$TenantName"
        Write-Verbose $URI
        
        Try {
            $JSON = Invoke-RestMethod -Method GET -WebSession $Global:NectarSession -uri $URI        
            
            If ($JSON.amount -eq 0) {
                Write-Error "Cannot find user with name $SearchQuery."
            }
            If ($TenantName) {$JSON.elements | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty} # Add the tenant name to the output which helps pipelining
            Return $JSON.elements
        }
        Catch {
            Write-Error "Cannot find user with name $SearchQuery."
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}


Function Get-NectarUserDetails {
    <#
        .SYNOPSIS
        Returns all information on a user.
 
        .DESCRIPTION
        Returns all information on a user.
         
        .PARAMETER EmailAddress
        The email address of the user.
     
        .PARAMETER Identity
        The numerical ID of the user. Can be obtained via Get-NectarUser and pipelined to Get-NectarUserDetails
         
        .EXAMPLE
        Get-NectarUserDetails tferguson@nectarcorp.com
         
        .NOTES
        Version 1.1
    #>

    
    [Alias("gnadud")]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias("email")]
        [string]$EmailAddress, 
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias("userId")]
        [string]$Identity
    )
    
    Begin {
        Connect-NectarCloud
    }        
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
        
        If ($EmailAddress -and !$Identity) {
            $Identity = (Get-NectarUser -SearchQuery $EmailAddress -Tenant $TenantName -ResultSize 1 -ErrorVariable GetUserError).userId
        }
            
        If (!$GetUserError) {
            $URI = "https://$Global:NectarCloud/dapi/user/$Identity/advanced?tenant=$TenantName"
            
            Try {
                $JSON = Invoke-RestMethod -Method GET -Credential $Global:NectarCred -uri $URI
                If ($TenantName) {$JSON | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty} # Add the tenant name to the output which helps pipelining
                Return $JSON
            }
            Catch {
                Write-Error "Unable to find user $EmailAddress. Ensure you typed the name of the user correctly."
                Get-JSONErrorStream -JSONResponse $_
            }
        }
    }
}




#################################################################################################################################################
# #
# Call Detail Functions #
# #
#################################################################################################################################################

Function Set-NectarFilterParams {
    <#
        .SYNOPSIS
        Sets the filter parameters used for querying call data
         
        .DESCRIPTION
        Sets the filter parameters used for querying call data
         
        .OUTPUTS
        WebSession cookie to use in other JSON requests
 
        .EXAMPLE
        Set-NectarFilterParams
 
        .NOTES
        Version 1.1
    #>


    [Alias("snfp")]
    Param (
        [parameter(Mandatory=$False)]
        [ValidateSet('LAST_HOUR','LAST_DAY','LAST_WEEK','LAST_MONTH','CUSTOM', IgnoreCase=$True)]
        [string]$TimePeriod = 'LAST_HOUR',
        [parameter(Mandatory=$False)]
        [Alias("StartDateFrom")]
        [string]$TimePeriodFrom,
        [parameter(Mandatory=$False)]
        [Alias("StartDateTo")]
        [string]$TimePeriodTo,
        [parameter(Mandatory=$False)]
        [ValidateSet('GOOD','POOR_0_25','PARTIALLY_GOOD_25_50','PARTIALLY_GOOD_50_75','PARTIALLY_GOOD_75_100','UNAVAILABLE','UNKNOWN', IgnoreCase=$True)]
        [string[]]$SessionQualities,
        [Parameter(Mandatory=$False)]
        [ValidateRange(0,99999999)]
        [int]$DurationFrom = 0,
        [Parameter(Mandatory=$False)]
        [ValidateRange(0,99999999)]
        [int]$DurationTo = 99999999,
        [parameter(Mandatory=$False)]
        [ValidateSet('AUDIO','VIDEO','APP_SHARING','FILE_TRANSFER','IM','UNKNOWN','TOTAL','VBSS','REMOTE_ASSISTANCE','APP_INVITE','FOCUS','UNKNOWN', IgnoreCase=$True)]
        [string[]]$Modalities,
        [parameter(Mandatory=$False)]
        [ValidateSet('TCP','UDP','Unknown', IgnoreCase=$False)]
        [string[]]$Protocols,
        [Parameter(Mandatory=$False)]
        [ValidateRange(0,608)]
        [string[]]$ResponseCodes,
        [parameter(Mandatory=$False)]
        [ValidateSet('INTERNAL','EXTERNAL','FEDERATED','INTERNAL_EXTERNAL','EXTERNAL_INTERNAL','FEDERATED_INTERNAL','INTERNAL_FEDERATED','FEDERATED_EXTERNAL','EXTERNAL_FEDERATED','UNKNOWN', IgnoreCase=$True)]
        [string[]]$SessionScenarios,
        [parameter(Mandatory=$False)]
        [ValidateSet('CONFERENCE','CONFERENCE_SESSION','PEER2PEER','PEER2PEER_MULTIMEDIA','PSTN', IgnoreCase=$False)]
        [string[]]$SessionTypes,
        [parameter(Mandatory=$False)]
        [string[]]$Codecs,
        [parameter(Mandatory=$False)]
        [string[]]$CallerCodecs,
        [parameter(Mandatory=$False)]
        [string[]]$CalleeCodecs,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$Devices,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerDevices,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeDevices,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CaptureDevices,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerCaptureDevices,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeCaptureDevices,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$RenderDevices,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerRenderDevices,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeRenderDevices,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$DeviceVersions,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerDeviceVersions,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeDeviceVersions,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ipaddress[]]$IPAddresses,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ipaddress[]]$CallerIPAddresses,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ipaddress[]]$CalleeIPAddresses,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$Locations,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerLocations,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeLocations,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ExtCities,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerExtCities,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeExtCities,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ExtCountries,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerExtCountries,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeExtCountries,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ExtISPs,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerExtISPs,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeExtISPs,
        [parameter(Mandatory=$False)]
        [ValidateSet('Enterprise WiFi','Enterprise Wired','External Mobile','External WiFi','External Wired','Unknown','Unavailable', IgnoreCase=$False)]
        [string[]]$NetworkTypes,
        [parameter(Mandatory=$False)]
        [ValidateSet('Enterprise WiFi','Enterprise Wired','External Mobile','External WiFi','External Wired','Unknown','Unavailable', IgnoreCase=$False)]
        [string[]]$CallerNetworkTypes,
        [parameter(Mandatory=$False)]
        [ValidateSet('Enterprise WiFi','Enterprise Wired','External Mobile','External WiFi','External Wired','Unknown','Unavailable', IgnoreCase=$False)]
        [string[]]$CalleeNetworkTypes,
        [parameter(Mandatory=$False)]
        [ValidateSet('SKYPE','CISCO','CISCO_CMS','CISCO_VKM','TEAMS','SKYPE_ONLINE','CISCO_CMS_VKM','AVAYA_AURA_CM','RIG','LYNC_VKM','SKYPE_FOR_BUSINESS_VKM','CISCO_UNITY','CISCO_EXPRESSWAY','AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','ZOOM','ENDPOINT_CLIENT','MISCELLANEOUS', IgnoreCase=$True)]
        [string]$Platform,
        [parameter(Mandatory=$False)]
        [ValidateSet('SKYPE','CISCO','CISCO_CMS','CISCO_VKM','TEAMS','SKYPE_ONLINE','CISCO_CMS_VKM','AVAYA_AURA_CM','RIG','LYNC_VKM','SKYPE_FOR_BUSINESS_VKM','CISCO_UNITY','CISCO_EXPRESSWAY','AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','ZOOM','ENDPOINT_CLIENT','MISCELLANEOUS', IgnoreCase=$True)]
        [string[]]$Platforms,
        [parameter(Mandatory=$False)]
        [ValidateSet('SKYPE','CISCO','CISCO_CMS','CISCO_VKM','TEAMS','SKYPE_ONLINE','CISCO_CMS_VKM','AVAYA_AURA_CM','RIG','LYNC_VKM','SKYPE_FOR_BUSINESS_VKM','CISCO_UNITY','CISCO_EXPRESSWAY','AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','ZOOM','ENDPOINT_CLIENT','MISCELLANEOUS', IgnoreCase=$True)]
        [string[]]$CallerPlatforms,
        [parameter(Mandatory=$False)]
        [ValidateSet('SKYPE','CISCO','CISCO_CMS','CISCO_VKM','TEAMS','SKYPE_ONLINE','CISCO_CMS_VKM','AVAYA_AURA_CM','RIG','LYNC_VKM','SKYPE_FOR_BUSINESS_VKM','CISCO_UNITY','CISCO_EXPRESSWAY','AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','ZOOM','ENDPOINT_CLIENT','MISCELLANEOUS', IgnoreCase=$True)]
        [string[]]$CalleePlatforms,
        [parameter(Mandatory=$False)]
        [ValidateSet('External','Internal','Federated','Unknown', IgnoreCase=$True)]
        [string[]]$Scenarios,
        [parameter(Mandatory=$False)]
        [ValidateSet('External','Internal','Federated','Unknown', IgnoreCase=$True)]
        [string[]]$CallerScenarios,
        [parameter(Mandatory=$False)]
        [ValidateSet('External','Internal','Federated','Unknown', IgnoreCase=$True)]
        [string[]]$CalleeScenarios,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ipaddress[]]$Subnets,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ipaddress[]]$CallerSubnets,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ipaddress[]]$CalleeSubnets,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$Users,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$FromUsers,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ToUsers,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ConfOrganizers,
        [parameter(Mandatory=$False)]
        [ValidateSet('TRUE','FALSE','UNKNOWN', IgnoreCase=$True)]
        [string[]]$VPN,
        [parameter(Mandatory=$False)]
        [ValidateSet('TRUE','FALSE','UNKNOWN', IgnoreCase=$True)]
        [string[]]$CallerVPN,
        [parameter(Mandatory=$False)]
        [ValidateSet('TRUE','FALSE','UNKNOWN', IgnoreCase=$True)]
        [string[]]$CalleeVPN,
        [parameter(Mandatory=$False)]
        [ValidateRange(0,99999)]
        [int]$ParticipantsMinCount,
        [parameter(Mandatory=$False)]
        [ValidateRange(0,99999)]
        [int]$ParticipantsMaxCount,
        [parameter(Mandatory=$False)]
        [ValidateSet('BAD','POOR','FAIR','GOOD','EXCELLENT', IgnoreCase=$False)]
        [string[]]$FeedbackRating,
        [parameter(Mandatory=$False)]
        [ValidateSet('P2P','PING','AUDIO','VIDEO', IgnoreCase=$False)]
        [string[]]$TestTypes,    
        [parameter(Mandatory=$False)]
        [ValidateSet('PASSED','FAILED','UNKNOWN', IgnoreCase=$False)]
        [string[]]$TestResults,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [parameter(Mandatory=$False)]
        [ValidateSet('DEFAULT','USERS','ENDPOINT_CLIENT', IgnoreCase=$True)]
        [string]$Scope = 'DEFAULT'
    )    

    
    Begin {
        Connect-NectarCloud
    }        
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
        
        # Convert to all-caps
        If($Scope) { $Scope = $Scope.ToUpper() }
        If($TimePeriod) { $TimePeriod = $TimePeriod.ToUpper() }    
        
        $FilterParams = @{
            'Scope' = $Scope
            'TimePeriod' = $TimePeriod
        }
    
        $TextInfo = (Get-Culture).TextInfo
    
        # Convert any PowerShell array objects to comma-separated strings to add to the GET querystring
        If ($SessionQualities) { $SessionQualities.ToUpper() | %{$SessionQualitiesStr += ($(if($SessionQualitiesStr){","}) + $_)}; $FilterParams.Add('SessionQualities',$SessionQualitiesStr) }
        If ($DurationFrom) { $FilterParams.Add('DurationFrom',$DurationFrom) }
        If ($DurationTo) { $FilterParams.Add('DurationTo',$DurationTo) }
        If ($Modalities) { $Modalities | %{$ModalitiesStr += ($(if($ModalitiesStr){","}) + $_)}; $FilterParams.Add('Modalities',$ModalitiesStr) }
        If ($Protocols) { $Protocols | %{$ProtocolsStr += ($(if($ProtocolsStr){","}) + $_)}; $FilterParams.Add('Protocols',$ProtocolsStr) }
        If ($ResponseCodes) { $ResponseCodes | %{$ResponseCodesStr += ($(if($ResponseCodesStr){","}) + $_)}; $FilterParams.Add('ResponseCodes',$ResponseCodesStr) }
        If ($SessionScenarios) { $SessionScenarios.ToUpper() | %{$SessionScenariosStr += ($(if($SessionScenariosStr){","}) + $_)}; $FilterParams.Add('SessionScenarios',$SessionScenariosStr) }
        If ($SessionTypes) { $SessionTypes | %{$SessionTypesStr += ($(if($SessionTypesStr){","}) + $_)}; $FilterParams.Add('SessionTypes',$SessionTypesStr) }
        If ($SessionTypes) { If ($SessionTypes.IndexOf('CONFERENCE_SESSION') -ge 0) { $FilterParams.Add('showConferenceSessions','true') } }
        If ($Codecs) { $Codecs | %{$CodecsStr += ($(if($CodecsStr){","}) + $_)}; $FilterParams.Add('Codecs',$CodecsStr) }
        If ($CallerCodecs) { $CallerCodecs | %{$CallerCodecsStr += ($(if($CallerCodecsStr){","}) + $_)}; $FilterParams.Add('CallerCodecs',$CallerCodecsStr) }
        If ($CalleeCodecs) { $CalleeCodecs | %{$CalleeCodecsStr += ($(if($CalleeCodecsStr){","}) + $_)}; $FilterParams.Add('CalleeCodecs',$CalleeCodecsStr) }
        If ($Devices) { $Devices | %{$DevicesStr += ($(if($DevicesStr){","}) + $_)}; $FilterParams.Add('Devices',$DevicesStr) }
        If ($CallerDevices) { $CallerDevices | %{$CallerDevicesStr += ($(if($CallerDevicesStr){","}) + $_)}; $FilterParams.Add('CallerDevices',$CallerDevicesStr) }
        If ($CalleeDevices) { $CalleeDevices | %{$CalleeDevicesStr += ($(if($CalleeDevicesStr){","}) + $_)}; $FilterParams.Add('CalleeDevices',$CalleeDevicesStr) }
        If ($CaptureDevices) { $CaptureDevices | %{$CaptureDevicesStr += ($(if($CaptureDevicesStr){","}) + $_)}; $FilterParams.Add('CaptureDevices',$CaptureDevicesStr) }
        If ($CallerCaptureDevices) { $CallerCaptureDevices | %{$CallerCaptureDevicesStr += ($(if($CallerCaptureDevicesStr){","}) + $_)}; $FilterParams.Add('CallerCaptureDevices',$CallerCaptureDevicesStr) }
        If ($CalleeCaptureDevices) { $CalleeCaptureDevices | %{$CalleeCaptureDevicesStr += ($(if($CalleeCaptureDevicesStr){","}) + $_)}; $FilterParams.Add('CalleeCaptureDevices',$CalleeCaptureDevicesStr) }
        If ($RenderDevices) { $RenderDevices | %{$RenderDevicesStr += ($(if($RenderDevicesStr){","}) + $_)}; $FilterParams.Add('RenderDevices',$RenderDevicesStr) }
        If ($CallerRenderDevices) { $CallerRenderDevices | %{$CallerRenderDevicesStr += ($(if($CallerRenderDevicesStr){","}) + $_)}; $FilterParams.Add('CallerRenderDevices',$CallerRenderDevicesStr) }
        If ($CalleeRenderDevices) { $CalleeRenderDevices | %{$CalleeRenderDevicesStr += ($(if($CalleeRenderDevicesStr){","}) + $_)}; $FilterParams.Add('CalleeRenderDevices',$CalleeRenderDevicesStr) }
        If ($DeviceVersions) { $DeviceVersions | %{$DeviceVersionsStr += ($(if($DeviceVersionsStr){","}) + $_)}; $FilterParams.Add('DeviceVersions',$DeviceVersionsStr) }
        If ($CallerDeviceVersions) { $CallerDeviceVersions | %{$CallerDeviceVersionsStr += ($(if($CallerDeviceVersionsStr){","}) + $_)}; $FilterParams.Add('CallerDeviceVersions',$CallerDeviceVersionsStr) }
        If ($CalleeDeviceVersions) { $CalleeDeviceVersions | %{$CalleeDeviceVersionsStr += ($(if($CalleeDeviceVersionsStr){","}) + $_)}; $FilterParams.Add('CalleeDeviceVersions',$CalleeDeviceVersionsStr) }
        If ($IPAddresses) { $IPAddresses | %{[string]$IPAddressesStr += ($(if([string]$IPAddressesStr){","}) + $_)}; $FilterParams.Add('IPAddresses',$IPAddressesStr) }
        If ($CallerIPAddresses) { $CallerIPAddresses | %{[string]$CallerIPAddressesStr += ($(if([string]$CallerIPAddressesStr){","}) + $_)}; $FilterParams.Add('CallerIPAddresses',$CallerIPAddressesStr) }
        If ($CalleeIPAddresses) { $CalleeIPAddresses | %{[string]$CalleeIPAddressesStr += ($(if([string]$CalleeIPAddressesStr){","}) + $_)}; $FilterParams.Add('CalleeIPAddresses',$CalleeIPAddressesStr) }
        If ($Locations) { $Locations | %{$LocationsStr += ($(if($LocationsStr){","}) + $_)}; $FilterParams.Add('Locations',$LocationsStr) }
        If ($CallerLocations) { $CallerLocations | %{$CallerLocationsStr += ($(if($CallerLocationsStr){","}) + $_)}; $FilterParams.Add('CallerLocations',$CallerLocationsStr) }
        If ($CalleeLocations) { $CalleeLocations | %{$CalleeLocationsStr += ($(if($CalleeLocationsStr){","}) + $_)}; $FilterParams.Add('CalleeLocations',$CalleeLocationsStr) }
        If ($ExtCities) { $ExtCities | %{$ExtCitiesStr += ($(if($ExtCitiesStr){","}) + $_)}; $FilterParams.Add('ExtCities',$ExtCitiesStr) }
        If ($CallerExtCities) { $CallerExtCities | %{$CallerExtCitiesStr += ($(if($CallerExtCitiesStr){","}) + $_)}; $FilterParams.Add('CallerExtCities',$CallerExtCitiesStr) }
        If ($CalleeExtCities) { $CalleeExtCities | %{$CalleeExtCitiesStr += ($(if($CalleeExtCitiesStr){","}) + $_)}; $FilterParams.Add('CalleeExtCities',$CalleeExtCitiesStr) }
        If ($ExtCountries) { $ExtCountries | %{$ExtCountriesStr += ($(if($ExtCountriesStr){","}) + $_)}; $FilterParams.Add('ExtCountries',$ExtCountriesStr) }
        If ($CallerExtCountries) { $CallerExtCountries | %{$CallerExtCountriesStr += ($(if($CallerExtCountriesStr){","}) + $_)}; $FilterParams.Add('CallerExtCountries',$CallerExtCountriesStr) }
        If ($CalleeExtCountries) { $CalleeExtCountries | %{$CalleeExtCountriesStr += ($(if($CalleeExtCountriesStr){","}) + $_)}; $FilterParams.Add('CalleeExtCountries',$CalleeExtCountriesStr) }
        If ($ExtISPs) { $ExtISPs | %{$ExtISPsStr += ($(if($ExtISPsStr){","}) + $_)}; $FilterParams.Add('extIsps',$ExtISPsStr) }
        If ($CallerExtISPs) { $CallerExtISPs | %{$CallerExtISPsStr += ($(if($CallerExtISPsStr){","}) + $_)}; $FilterParams.Add('callerExtIsps',$CallerExtISPsStr) }
        If ($CalleeExtISPs) { $CalleeExtISPs | %{$CalleeExtISPsStr += ($(if($CalleeExtISPsStr){","}) + $_)}; $FilterParams.Add('calleeExtIsps',$CalleeExtISPsStr) }
        If ($NetworkTypes) { $NetworkTypes | %{$NetworkTypesStr += ($(if($NetworkTypesStr){","}) + $_)}; $FilterParams.Add('NetworkTypes',$NetworkTypesStr) }
        If ($CallerNetworkTypes) { $CallerNetworkTypes | %{$CallerNetworkTypesStr += ($(if($CallerNetworkTypesStr){","}) + $_)}; $FilterParams.Add('CallerNetworkTypes',$CallerNetworkTypesStr) }
        If ($CalleeNetworkTypes) { $CalleeNetworkTypes | %{$CalleeNetworkTypesStr += ($(if($CalleeNetworkTypesStr){","}) + $_)}; $FilterParams.Add('CalleeNetworkTypes',$CalleeNetworkTypesStr) }
        If ($Platform) { $FilterParams.Add('platform',$Platform) }
        If ($Platforms) { $Platforms.ToUpper() | %{$PlatformsStr += ($(if($PlatformsStr){","}) + $_)}; $FilterParams.Add('Platforms',$PlatformsStr) }
        If ($CallerPlatforms) { $CallerPlatforms.ToUpper() | %{$CallerPlatformsStr += ($(if($CallerPlatformsStr){","}) + $_)}; $FilterParams.Add('CallerPlatforms',$CallerPlatformsStr) }
        If ($CalleePlatforms) { $CalleePlatforms.ToUpper() | %{$CalleePlatformsStr += ($(if($CalleePlatformsStr){","}) + $_)}; $FilterParams.Add('CalleePlatforms',$CalleePlatformsStr) }
        If ($Scenarios) { $Scenarios.ToUpper() | %{$ScenariosStr += ($(if($ScenariosStr){","}) + $_)}; $FilterParams.Add('Scenarios',$ScenariosStr) }
        If ($CallerScenarios) { $CallerScenarios.ToUpper() | %{$CallerScenariosStr += ($(if($CallerScenariosStr){","}) + $_)}; $FilterParams.Add('CallerScenarios',$CallerScenariosStr) }
        If ($CalleeScenarios) { $CalleeScenarios.ToUpper() | %{$CalleeScenariosStr += ($(if($CalleeScenariosStr){","}) + $_)}; $FilterParams.Add('CalleeScenarios',$CalleeScenariosStr) }
        If ($Subnets) { $Subnets | %{[string]$SubnetsStr += ($(if([string]$SubnetsStr){","}) + $_)}; $FilterParams.Add('Subnets',$SubnetsStr) }
        If ($CallerSubnets) { $CallerSubnets | %{[string]$CallerSubnetsStr += ($(if([string]$CallerSubnetsStr){","}) + $_)}; $FilterParams.Add('CallerSubnets',$CallerSubnetsStr) }
        If ($CalleeSubnets) { $CalleeSubnets | %{[string]$CalleeSubnetsStr += ($(if($CalleeSubnetsStr){","}) + $_)}; $FilterParams.Add('CalleeSubnets',$CalleeSubnetsStr) }
        If ($VPN) { $VPN.ToUpper() | %{$VPNStr += ($(if($VPNStr){","}) + $_)}; $FilterParams.Add('Vpn',$VPNStr) }
        If ($CallerVPN) { $CallerVPN.ToUpper() | %{$CallerVPNStr += ($(if($CallerVPNStr){","}) + $_)}; $FilterParams.Add('CallerVpn',$VPNStr) }
        If ($CalleeVPN) { $CalleeVPN.ToUpper() | %{$CalleeVPNStr += ($(if($CalleeVPNStr){","}) + $_)}; $FilterParams.Add('CalleeVpn',$VPNStr) }
        If ($ParticipantsMinCount) { $FilterParams.Add('participantsMinCount',$ParticipantsMinCount) }
        If ($ParticipantsMaxCount) { $FilterParams.Add('participantsMaxCount',$ParticipantsMaxCount) }
        If ($FeedbackRating) { $FeedbackRating.ToUpper() | %{$FeedbackRatingStr += ($(if($FeedbackRatingStr){","}) + $_)}; $FilterParams.Add('Ratings',$FeedbackRatingStr) }
        If ($TestTypes) { $TestTypes.ToUpper() | %{$TestTypeStr += ($(if($TestTypeStr){","}) + $_)}; $FilterParams.Add('testTypes',$TestTypeStr) }
        If ($TestResults) { $TestResults.ToUpper() | %{$TestResultsStr += ($(if($TestResultsStr){","}) + $_)}; $FilterParams.Add('testResults',$TestResultsStr) }
        If ($TenantName) { $FilterParams.Add('Tenant',$TenantName) }

        # Get the user IDs for any entered users
        If ($Users) {
            $UserIDs = @()
            ForEach($User in $Users) {
                $UserIDs += (Get-NectarUser $User -TenantName $TenantName -ErrorAction:Stop).id
            }

            $UserIDs | %{$UserIDsStr += ($(if($UserIDsStr){","}) + $_)}
            $FilterParams.Add('Users',$UserIDsStr)
        }

        If ($FromUsers) {
            $FromUserIDs = @()
            ForEach($User in $FromUsers) {
                $FromUserIDs += (Get-NectarUser $User -TenantName $TenantName -ErrorAction:Stop).id
            }
            $FromUserIDs | %{$FromUserIDsStr += ($(if($FromUserIDsStr){","}) + $_)}
            $FilterParams.Add('FromUsers',$FromUserIDsStr)
        }
        
        If ($ToUsers) {
            $ToUserIDs = @()
            ForEach($User in $ToUsers) {
                $ToUserIDs += (Get-NectarUser $User -TenantName $TenantName -ErrorAction:Stop).id
            }
            $ToUserIDs | %{$ToUserIDsStr += ($(if($ToUserIDsStr){","}) + $_)}
            $FilterParams.Add('ToUsers',$ToUserIDsStr)
        }

        If ($ConfOrganizers) {
            $ConfOrganizerIDs = @()
            ForEach($Organizer in $ConfOrganizers) {
                $ConfOrganizerIDs += (Get-NectarUser $Organizer -TenantName $TenantName -ErrorAction:Stop).id
            }
            $ConfOrganizerIDs | %{$ConfOrganizerIDsStr += ($(if($ConfOrganizerIDsStr){","}) + $_)}
            $FilterParams.Add('organizersOrSpaces',$ConfOrganizerIDsStr)
        }

        # Convert date to UNIX timestamp
        If ($TimePeriodFrom) {
            $TimePeriodFrom = (Get-Date -Date $TimePeriodFrom -UFormat %s) + '000'
            $FilterParams.Add('StartDateFrom',$TimePeriodFrom)
            Write-Verbose "TimePeriodFrom: $TimePeriodFrom"
        }
        
        If ($TimePeriodTo) {
            $TimePeriodTo = (Get-Date -Date $TimePeriodTo -UFormat %s) + '000'
            $FilterParams.Add('StartDateTo',$TimePeriodTo)
            Write-Verbose "TimePeriodTo: $TimePeriodTo"
        }

        # Try {
            # Run the filter POST and obtain the session cookie for the GET
            $URI = "https://$Global:NectarCloud/dapi/filter/apply"
            $FilterResults = Invoke-WebRequest -Method POST -Credential $Global:NectarCred -uri $URI -Body $FilterParams -UseBasicParsing -SessionVariable Session
            $Cookie = $Session.Cookies.GetCookies($URI) | Where {$_.Name -eq 'SESSION'} 
            $FilterSession = New-Object Microsoft.PowerShell.Commands.WebRequestSession
            $FilterSession.Cookies.Add($Cookie)
            Write-Verbose "Successfully set filter parameters."    
            Write-Verbose $Uri
            Write-Verbose $FilterResults
            Return $FilterSession
        # }
        # Catch {
            # Write-Error "Unable to set filter parameters."
            # (Get-JSONErrorStream -JSONResponse $_).Replace("startDate","TimePeriod")
        # }
    }
}


Function Get-NectarSessions {
    <#
        .SYNOPSIS
        Returns all session information.
 
        .DESCRIPTION
        Returns all session information as presented on the SESSION LIST section of the CALL DETAILS page
        UI_ELEMENT
         
        .PARAMETER TimePeriod
        The time period to show session data from. Select from 'LAST_HOUR','LAST_DAY','LAST_WEEK','LAST_MONTH','CUSTOM'.
        CUSTOM requires using TimePeriodFrom and TimePeriodTo parameters.
         
        .PARAMETER TimePeriodFrom
        The earliest date/time to show session data from. Must be used in conjunction with -TimePeriod CUSTOM and TimePeriodTo parameters. Use format 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'. All time/dates in UTC.
         
        .PARAMETER TimePeriodTo
        The latest date/time to show session data from. Must be used in conjunction with -TimePeriod CUSTOM and TimePeriodFrom parameters. Use format 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'. All time/dates in UTC.
         
        .PARAMETER SessionQualities
        Show sessions that match a given quality rating. Case sensitive. Choose one or more from:
        'GOOD','POOR_0_25','PARTIALLY_GOOD_25_50','PARTIALLY_GOOD_50_75','PARTIALLY_GOOD_75_100','UNAVAILABLE','UNKNOWN'
         
        .PARAMETER DurationFrom
        The shortest call length (in seconds) to show session data.
         
        .PARAMETER DurationTo
        The longest call length (in seconds) to show session data.
         
        .PARAMETER Modalities
        Show sessions that match one or more modality types. Not case sensitive. Choose one or more from:
        'AUDIO','VIDEO','APP_SHARING','FILE_TRANSFER','IM','UNKNOWN','TOTAL','VBSS','REMOTE_ASSISTANCE','APP_INVITE','FOCUS','UNKNOWN'
         
        .PARAMETER Protocols
        Show sessions that match one or more network protocol types. Case sensitive. Choose one or more from:
        'TCP','UDP','Unknown'
         
        .PARAMETER ResponseCodes
        Show sessions that match one or more SIP response codes. Accepts numbers from 200 to 699
         
        .PARAMETER SessionScenarios
        Show sessions that match one or more session scenarios. Not case sensitive. Choose one or more from:
        'External','Internal','Internal-External','External-Internal','Federated','Internal-Federated','External-Federated','Unknown'
         
        .PARAMETER SessionTypes
        Show sessions that match one or more session scenarios. Case sensitive. Choose one or more from:
        'Conference','Peer To Peer','Peer To Peer (Multimedia)','PSTN/External'
         
        .PARAMETER Codecs
        Show sessions where the selected codec was used by either caller or callee. Can query for multiple codecs. Case sensitive. Use Get-NectarCodecs for a list of valid codecs.
 
        .PARAMETER CallerCodecs
        Show sessions where the selected codec was used by the caller. Can query for multiple codecs. Case sensitive. Use Get-NectarCodecs for a list of valid codecs.
         
        .PARAMETER CalleeCodecs
        Show sessions where the selected codec was used by the callee. Can query for multiple codecs. Case sensitive. Use Get-NectarCodecs for a list of valid codecs.
         
        .PARAMETER Devices
        Show sessions where the selected device was used by either caller or callee. Can query for multiple devices. Case sensitive. Use Get-NectarSupportedDevice for a list of valid devices.
 
        .PARAMETER CallerDevices
        Show sessions where the selected device was used by the caller. Can query for multiple devices. Case sensitive. Use Get-NectarSupportedDevice for a list of valid devices.
         
        .PARAMETER CalleeDevices
        Show sessions where the selected device was used by the callee. Can query for multiple devices. Case sensitive. Use Get-NectarSupportedDevice for a list of valid devices.
 
        .PARAMETER CaptureDevices
        Show sessions where the selected capture device (microphone) was used by either caller or callee. Can query for multiple devices. Case sensitive. Use Get-NectarSupportedDevice for a list of valid devices.
 
        .PARAMETER CallerCaptureDevices
        Show sessions where the selected capture device (microphone) was used by the caller. Can query for multiple devices. Case sensitive. Use Get-NectarSupportedDevice for a list of valid devices.
         
        .PARAMETER CalleeCaptureDevices
        Show sessions where the selected capture device (microphone) was used by the callee. Can query for multiple devices. Case sensitive. Use Get-NectarSupportedDevice for a list of valid devices.
 
        .PARAMETER RenderDevices
        Show sessions where the selected render device (speaker) was used by either caller or callee. Can query for multiple devices. Case sensitive. Use Get-NectarSupportedDevice for a list of valid devices.
 
        .PARAMETER CallerRenderDevices
        Show sessions where the selected render device (speaker) was used by the caller. Can query for multiple devices. Case sensitive. Use Get-NectarSupportedDevice for a list of valid devices.
         
        .PARAMETER CalleeRenderDevices
        Show sessions where the selected render device (speaker) was used by the callee. Can query for multiple devices. Case sensitive. Use Get-NectarSupportedDevice for a list of valid devices.
 
        .PARAMETER DeviceVersions
        Show sessions where the selected device version was used by either caller or callee. Can query for multiple devices. Case sensitive. Use Get-NectarClientVersion for a list of valid client versions.
 
        .PARAMETER CallerDeviceVersions
        Show sessions where the selected device version was used by the caller. Can query for multiple devices. Case sensitive. Use Get-NectarClientVersion for a list of valid client versions.
         
        .PARAMETER CalleeDeviceVersions
        Show sessions where the selected device version was used by the callee. Can query for multiple devices. Case sensitive. Use Get-NectarClientVersion for a list of valid client versions.
 
        .PARAMETER IPAddresses
        Show sessions where the selected IP address was used by either caller or callee. Can query for multiple IPs.
 
        .PARAMETER CallerIPAddresses
        Show sessions where the selected IP address was used by the caller. Can query for multiple IPs.
         
        .PARAMETER CalleeIPAddresses
        Show sessions where the selected IP address was used by the callee. Can query for multiple IPs.
         
        .PARAMETER Locations
        Show sessions where the selected location was used by either caller or callee. Can query for multiple locations.
 
        .PARAMETER CallerLocations
        Show sessions where the selected location was used by the caller. Can query for multiple locations.
         
        .PARAMETER CalleeLocations
        Show sessions where the selected location was used by the callee. Can query for multiple locations.
         
        .PARAMETER ExtCities
        Show sessions where the caller or callee was located in the selected city (as detected via geolocating the user's external IP address). Can query for multiple cities.
 
        .PARAMETER CallerExtCities
        Show sessions where the caller was located in the selected city (as detected via geolocating the user's external IP address). Can query for multiple cities.
         
        .PARAMETER CalleeExtCities
        Show sessions where the callee was located in the selected city (as detected via geolocating the user's external IP address). Can query for multiple cities.
         
        .PARAMETER ExtCountries
        Show sessions where the caller or callee was located in the selected country (as detected via geolocating the user's external IP address). Can query for multiple countries.
 
        .PARAMETER CallerExtCountries
        Show sessions where the caller was located in the selected country (as detected via geolocating the user's external IP address). Can query for multiple countries.
         
        .PARAMETER CalleeExtCountries
        Show sessions where the callee was located in the selected country (as detected via geolocating the user's external IP address). Can query for multiple countries.
         
        .PARAMETER ExtISPs
        Show sessions where the caller or callee was located in the selected ISP (as detected via geolocating the user's external IP address). Can query for multiple ISPs.
 
        .PARAMETER CallerExtISPs
        Show sessions where the caller was located in the selected ISP (as detected via geolocating the user's external IP address). Can query for multiple ISPs.
         
        .PARAMETER CalleeExtISPs
        Show sessions where the callee was located in the selected ISP (as detected via geolocating the user's external IP address). Can query for multiple ISPs.
         
        .PARAMETER NetworkTypes
        Show sessions where the selected network type was used by either caller or callee. Can query for multiple network types. Case sensitive. Choose one or more from:
        'Enterprise WiFi','Enterprise Wired','External Mobile','External WiFi','External Wired','Unknown','Unavailable'
 
        .PARAMETER CallerNetworkTypes
        Show sessions where the selected network type was used by the caller. Can query for multiple network types. Case sensitive. Choose one or more from:
        'Enterprise WiFi','Enterprise Wired','External Mobile','External WiFi','External Wired','Unknown','Unavailable'
         
        .PARAMETER CalleeNetworkTypes
        Show sessions where the selected network type was used by the callee. Can query for multiple network types. Case sensitive. Choose one or more from:
        'Enterprise WiFi','Enterprise Wired','External Mobile','External WiFi','External Wired','Unknown','Unavailable'
 
        .PARAMETER Platforms
        Show sessions where the selected platform was used by either caller or callee. Can query for multiple platforms. Case sensitive. Choose one or more from:
        'SKYPE','CISCO','CISCO_CMS','CISCO_VKM','TEAMS','SKYPE_ONLINE','CISCO_CMS_VKM','AVAYA_AURA_CM','RIG','LYNC_VKM','SKYPE_FOR_BUSINESS_VKM','CISCO_UNITY','CISCO_EXPRESSWAY','AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','ZOOM','ENDPOINT_CLIENT','MISCELLANEOUS'
 
        .PARAMETER CallerPlatforms
        Show sessions where the selected platform was used by the caller. Can query for multiple platforms. Case sensitive. Choose one or more from:
        'SKYPE','CISCO','CISCO_CMS','CISCO_VKM','TEAMS','SKYPE_ONLINE','CISCO_CMS_VKM','AVAYA_AURA_CM','RIG','LYNC_VKM','SKYPE_FOR_BUSINESS_VKM','CISCO_UNITY','CISCO_EXPRESSWAY','AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','ZOOM','ENDPOINT_CLIENT','MISCELLANEOUS'
         
        .PARAMETER CalleePlatforms
        Show sessions where the selected platform was used by the callee. Can query for multiple platforms. Case sensitive. Choose one or more from:
        'SKYPE','CISCO','CISCO_CMS','CISCO_VKM','TEAMS','SKYPE_ONLINE','CISCO_CMS_VKM','AVAYA_AURA_CM','RIG','LYNC_VKM','SKYPE_FOR_BUSINESS_VKM','CISCO_UNITY','CISCO_EXPRESSWAY','AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','ZOOM','ENDPOINT_CLIENT','MISCELLANEOUS'
 
        .PARAMETER Scenarios
        Show sessions where the selected scenario was used by either caller or callee. Can query for multiple scenarios. Choose one or more from:
        'External','Internal','Federated','Unknown'
 
        .PARAMETER CallerScenarios
        Show sessions where the selected scenario was used by the caller. Can query for multiple scenarios. Choose one or more from:
        'External','Internal','Federated','Unknown'
         
        .PARAMETER CalleeScenarios
        Show sessions where the selected scenario was used by the callee. Can query for multiple scenarios. Choose one or more from:
        'External','Internal','Federated','Unknown'
 
        .PARAMETER Subnets
        Show sessions where the selected subnet was used by either caller or callee. Can query for multiple subnets.
 
        .PARAMETER CallerSubnets
        Show sessions where the selected subnet was used by the caller. Can query for multiple subnets.
         
        .PARAMETER CalleeSubnets
        Show sessions where the selected subnet was used by the callee. Can query for multiple subnets.
         
        .PARAMETER Users
        Show sessions where the selected user was either caller or callee. Can query for multiple users.
 
        .PARAMETER FromUsers
        Show sessions where the selected user was the caller. Can query for multiple users.
         
        .PARAMETER ToUsers
        Show sessions where the selected user was the callee. Can query for multiple users.
 
        .PARAMETER ConfOrganizers
        Show sessions hosted by a specified conference organizer. Can query for multiple organizers.
         
        .PARAMETER VPN
        Show sessions where the selected VPN was used by either caller or callee.
 
        .PARAMETER CallerVPN
        Show sessions where the selected VPN was used by the caller.
         
        .PARAMETER CalleeVPN
        Show sessions where the selected VPN was used by the callee.
         
        .PARAMETER ParticipantsMinCount
        Show sessions where the number of participants is greater than or equal to the entered value
 
        .PARAMETER ParticipantsMaxCount
        Show sessions where the number of participants is less than or equal to the entered value
         
        .PARAMETER FeedbackRating
        Show sessions where users provided specific feedback ratings from BAD to EXCELLENT.
        Allowed values are BAD, POOR, FAIR, GOOD, EXCELLENT. Corresponds to ratings from 1 to 5 stars.
         
        .PARAMETER OrderByField
        Sort the output by the selected field
         
        .PARAMETER OrderDirection
        Sort direction. Use with OrderByField. Not case sensitive. Choose from:
        ASC, DESC
 
        .PARAMETER TenantName
        The name of the Nectar 10 tenant. Used in multi-tenant configurations.
         
        .PARAMETER PageSize
        The size of the page used to return data. Defaults to 1000
         
        .PARAMETER ResultSize
        The total number of results to return. Defaults to 1000. Maximum result size is 9,999,999 results
         
        .EXAMPLE
        Get-NectarSessions -TimePeriod LAST_HOUR -Platforms TEAMS -Modalities AUDIO -SessionQualities POOR
        Returns a list of all Teams audio sessions for the past hour where the quality was rated Poor
         
        .EXAMPLE
        (Get-NectarSessions -SessionTypes CONFERENCE -TimePeriod CUSTOM -TimePeriodFrom '2021-05-06' -TimePeriodTo '2021-05-07').Count
        Returns a count of all conferences between May 6 and 7 (all times/dates UTC)
         
        .EXAMPLE
        Get-NectarSessions -SessionTypes PEER2PEER,PEER2PEER_MULTIMEDIA -TimePeriod CUSTOM -TimePeriodFrom '2021-05-06 14:00' -TimePeriodTo '2021-05-06 15:00'
        Returns a list of all P2P calls between 14:00 and 15:00 UTC on May 6
 
        .EXAMPLE
        Get-NectarSessions -TimePeriod LAST_WEEK -SessionTypes CONFERENCE | Select-Object confOrganizerOrSpace | Group-Object confOrganizerOrSpace | Select-Object Name, Count | Sort-Object Count -Descending
        Returns a list of conference organizers and a count of the total conferences organized by each, sorted by count.
         
        .NOTES
        Version 1.2
    #>

    
    [Alias("gns")]
    [CmdletBinding(PositionalBinding=$False)]
    Param (
        [parameter(Mandatory=$False)]
        [ValidateSet('LAST_HOUR','LAST_DAY','LAST_WEEK','LAST_MONTH','CUSTOM', IgnoreCase=$True)]
        [string]$TimePeriod = 'LAST_HOUR',
        [parameter(Mandatory=$False)]
        [Alias("StartDateFrom")]
        [string]$TimePeriodFrom,
        [parameter(Mandatory=$False)]
        [Alias("StartDateTo")]
        [string]$TimePeriodTo,
        [parameter(Mandatory=$False)]
        [ValidateSet('GOOD','POOR_0_25','PARTIALLY_GOOD_25_50','PARTIALLY_GOOD_50_75','PARTIALLY_GOOD_75_100','UNAVAILABLE','UNKNOWN', IgnoreCase=$True)]
        [string[]]$SessionQualities,
        [Parameter(Mandatory=$False)]
        [ValidateRange(0,99999999)]
        [int]$DurationFrom = 0,
        [Parameter(Mandatory=$False)]
        [ValidateRange(0,99999999)]
        [int]$DurationTo = 99999999,
        [parameter(Mandatory=$False)]
        [ValidateSet('AUDIO','VIDEO','APP_SHARING','FILE_TRANSFER','IM','UNKNOWN','TOTAL','VBSS','REMOTE_ASSISTANCE','APP_INVITE','FOCUS','UNKNOWN', IgnoreCase=$True)]
        [string[]]$Modalities,
        [parameter(Mandatory=$False)]
        [ValidateSet('TCP','UDP','Unknown', IgnoreCase=$False)]
        [string[]]$Protocols,
        [Parameter(Mandatory=$False)]
        [ValidateRange(200,699)]
        [string[]]$ResponseCodes,
        [parameter(Mandatory=$False)]
        [ValidateSet('INTERNAL','EXTERNAL','FEDERATED','INTERNAL_EXTERNAL','EXTERNAL_INTERNAL','FEDERATED_INTERNAL','INTERNAL_FEDERATED','FEDERATED_EXTERNAL','EXTERNAL_FEDERATED','UNKNOWN', IgnoreCase=$True)]
        [string[]]$SessionScenarios,
        [parameter(Mandatory=$False)]
        [ValidateSet('CONFERENCE','CONFERENCE_SESSION','PEER2PEER','PEER2PEER_MULTIMEDIA','PSTN', IgnoreCase=$False)]
        [string[]]$SessionTypes,
        [parameter(Mandatory=$False)]
        [string[]]$Codecs,
        [parameter(Mandatory=$False)]
        [string[]]$CallerCodecs,
        [parameter(Mandatory=$False)]
        [string[]]$CalleeCodecs,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$Devices,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerDevices,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeDevices,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CaptureDevices,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerCaptureDevices,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeCaptureDevices,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$RenderDevices,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerRenderDevices,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeRenderDevices,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$DeviceVersions,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerDeviceVersions,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeDeviceVersions,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ipaddress[]]$IPAddresses,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ipaddress[]]$CallerIPAddresses,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ipaddress[]]$CalleeIPAddresses,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$Locations,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerLocations,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeLocations,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ExtCities,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerExtCities,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeExtCities,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ExtCountries,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerExtCountries,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeExtCountries,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ExtISPs,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerExtISPs,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeExtISPs,
        [parameter(Mandatory=$False)]
        [ValidateSet('Enterprise WiFi','Enterprise Wired','External Mobile','External WiFi','External Wired','Unknown','Unavailable', IgnoreCase=$False)]
        [string[]]$NetworkTypes,
        [parameter(Mandatory=$False)]
        [ValidateSet('Enterprise WiFi','Enterprise Wired','External Mobile','External WiFi','External Wired','Unknown','Unavailable', IgnoreCase=$False)]
        [string[]]$CallerNetworkTypes,
        [parameter(Mandatory=$False)]
        [ValidateSet('Enterprise WiFi','Enterprise Wired','External Mobile','External WiFi','External Wired','Unknown','Unavailable', IgnoreCase=$False)]
        [string[]]$CalleeNetworkTypes,
        [parameter(Mandatory=$False)]
        [ValidateSet('SKYPE','CISCO','CISCO_CMS','CISCO_VKM','TEAMS','SKYPE_ONLINE','CISCO_CMS_VKM','AVAYA_AURA_CM','RIG','LYNC_VKM','SKYPE_FOR_BUSINESS_VKM','CISCO_UNITY','CISCO_EXPRESSWAY','AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','ZOOM','ENDPOINT_CLIENT','MISCELLANEOUS', IgnoreCase=$True)]
        [string[]]$Platforms,
        [parameter(Mandatory=$False)]
        [ValidateSet('SKYPE','CISCO','CISCO_CMS','CISCO_VKM','TEAMS','SKYPE_ONLINE','CISCO_CMS_VKM','AVAYA_AURA_CM','RIG','LYNC_VKM','SKYPE_FOR_BUSINESS_VKM','CISCO_UNITY','CISCO_EXPRESSWAY','AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','ZOOM','ENDPOINT_CLIENT','MISCELLANEOUS', IgnoreCase=$True)]
        [string[]]$CallerPlatforms,
        [parameter(Mandatory=$False)]
        [ValidateSet('SKYPE','CISCO','CISCO_CMS','CISCO_VKM','TEAMS','SKYPE_ONLINE','CISCO_CMS_VKM','AVAYA_AURA_CM','RIG','LYNC_VKM','SKYPE_FOR_BUSINESS_VKM','CISCO_UNITY','CISCO_EXPRESSWAY','AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','ZOOM','ENDPOINT_CLIENT','MISCELLANEOUS', IgnoreCase=$True)]
        [string[]]$CalleePlatforms,
        [parameter(Mandatory=$False)]
        [ValidateSet('External','Internal','Federated','Unknown', IgnoreCase=$True)]
        [string[]]$Scenarios,
        [parameter(Mandatory=$False)]
        [ValidateSet('External','Internal','Federated','Unknown', IgnoreCase=$True)]
        [string[]]$CallerScenarios,
        [parameter(Mandatory=$False)]
        [ValidateSet('External','Internal','Federated','Unknown', IgnoreCase=$True)]
        [string[]]$CalleeScenarios,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ipaddress[]]$Subnets,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ipaddress[]]$CallerSubnets,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ipaddress[]]$CalleeSubnets,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$Users,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$FromUsers,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ToUsers,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ConfOrganizers,        
        [parameter(Mandatory=$False)]
        [ValidateSet('TRUE','FALSE','UNKNOWN', IgnoreCase=$True)]
        [string[]]$VPN,
        [parameter(Mandatory=$False)]
        [ValidateSet('TRUE','FALSE','UNKNOWN', IgnoreCase=$True)]
        [string[]]$CallerVPN,
        [parameter(Mandatory=$False)]
        [ValidateSet('TRUE','FALSE','UNKNOWN', IgnoreCase=$True)]
        [string[]]$CalleeVPN,
        [parameter(Mandatory=$False)]
        [ValidateRange(0,99999)]
        [int]$ParticipantsMinCount,
        [parameter(Mandatory=$False)]
        [ValidateRange(0,99999)]
        [int]$ParticipantsMaxCount,
        [parameter(Mandatory=$False)]
        [ValidateSet('BAD','POOR','FAIR','GOOD','EXCELLENT', IgnoreCase=$False)]
        [string[]]$FeedbackRating,
        [parameter(Mandatory=$False)]
        [string]$OrderByField,
        [parameter(Mandatory=$False)]
        [ValidateSet('ASC','DESC', IgnoreCase=$True)]
        [string]$OrderDirection,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [parameter(Mandatory=$False)]
        [ValidateSet('DEFAULT','USERS', IgnoreCase=$True)]
        [string]$Scope = 'DEFAULT',
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,10000)]
        [int]$PageSize = 1000,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,50000)]
        [int]$ResultSize
    )
    
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
        
        If ($PageSize) { $PSBoundParameters.Remove('PageSize') | Out-Null }
        If ($ResultSize) { $PSBoundParameters.Remove('ResultSize') | Out-Null }
        If ($OrderByField) { $PSBoundParameters.Remove('OrderByField') | Out-Null }
        If ($OrderDirection) { $PSBoundParameters.Remove('OrderDirection') | Out-Null }
        
        $FilterSession = Set-NectarFilterParams @PsBoundParameters
        
        $URI = "https://$Global:NectarCloud/dapi/session"
        
        # Set the page size to the result size if -ResultSize switch is used to limit the number of returned items
        # Otherwise, set page size (defaults to 1000)
        If ($ResultSize) {
            $Params = @{ 'pageSize' = $ResultSize }
        }
        Else {
            $Params = @{ 'pageSize' = $PageSize }
        }
        
        If($OrderByField) { $Params.Add('OrderByField',$OrderByField) }
        If($OrderDirection) { $Params.Add('OrderDirection',$OrderDirection) }
        If ($TenantName) { $Params.Add('Tenant',$TenantName) }
        
        If ($SessionTypes) {
            If ($SessionTypes.IndexOf('CONFERENCE_SESSION') -ge 0) { 
                $Params.Add('showConferenceSessions','true') | Out-Null 
            }
            Else {
                $Params.Add('showConferenceSessions','false') | Out-Null 
            }
        }
        Else {
            $Params.Add('showConferenceSessions','false') | Out-Null
        }
        
        # Return results in pages
        Try {
            Write-Verbose $URI
            Write-Verbose $TimePeriodFrom
            Write-Verbose $TimePeriodTo
            $JSON = Invoke-RestMethod -Method GET -uri $URI -Body $Params -WebSession $FilterSession
            
            $TotalPages = $JSON.totalPages
            
            If ($TenantName) {$JSON.elements | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty}
            $JSON.elements | Add-Member -TypeName 'Nectar.SessionList'
            $JSON.elements
            
            If ($TotalPages -gt 1 -and !($ResultSize)) {
                $PageNum = 2
                Write-Verbose "Page size: $PageSize"
                While ($PageNum -le $TotalPages) {
                    Write-Verbose "Working on page $PageNum of $TotalPages"
                    $PagedURI = $URI + "?pageNumber=$PageNum"
                    $JSON = Invoke-RestMethod -Method GET -uri $PagedURI -Body $Params -WebSession $FilterSession
                    If ($TenantName) {$JSON.elements | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty}
                    $JSON.elements | Add-Member -TypeName 'Nectar.SessionList'
                    $JSON.elements
                    $PageNum++
                }
            }
        }
        Catch {
            Write-Error "No results. Try specifying a less-restrictive filter"
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}




Function Get-NectarSessionHistograms {
    <#
        .SYNOPSIS
        Returns session histogram for a given timeframe.
         
        .DESCRIPTION
        Returns session histogram for a given timeframe. This returns the numbers used to build the chart in the SESSIONS section of the CALL DETAILS screen
        UI_ELEMENT
         
        .PARAMETER TimePeriod
        The time period to show session data from. Select from 'LAST_HOUR','LAST_DAY','LAST_WEEK','LAST_MONTH','CUSTOM'.
        CUSTOM requires using TimePeriodFrom and TimePeriodTo parameters.
         
        .PARAMETER TimePeriodFrom
        The earliest date/time to show session data from. Must be used in conjunction with -TimePeriod CUSTOM and TimePeriodTo parameters. Use format 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'. All time/dates in UTC.
         
        .PARAMETER TimePeriodTo
        The latest date/time to show session data from. Must be used in conjunction with -TimePeriod CUSTOM and TimePeriodFrom parameters. Use format 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'. All time/dates in UTC.
         
        .PARAMETER SessionQualities
        Show sessions that match a given quality rating. Case sensitive. Choose one or more from:
        'GOOD','POOR_0_25','PARTIALLY_GOOD_25_50','PARTIALLY_GOOD_50_75','PARTIALLY_GOOD_75_100','UNAVAILABLE','UNKNOWN'
         
        .PARAMETER DurationFrom
        The shortest call length (in seconds) to show session data.
         
        .PARAMETER DurationTo
        The longest call length (in seconds) to show session data.
         
        .PARAMETER Modalities
        Show sessions that match one or more modality types. Not case sensitive. Choose one or more from:
        'AUDIO','VIDEO','APP_SHARING','FILE_TRANSFER','IM','UNKNOWN','TOTAL','VBSS','REMOTE_ASSISTANCE','APP_INVITE','FOCUS','UNKNOWN'
         
        .PARAMETER Protocols
        Show sessions that match one or more network protocol types. Case sensitive. Choose one or more from:
        'TCP','UDP','Unknown'
         
        .PARAMETER ResponseCodes
        Show sessions that match one or more SIP response codes. Accepts numbers from 200 to 699
         
        .PARAMETER SessionScenarios
        Show sessions that match one or more session scenarios. Not case sensitive. Choose one or more from:
        'External','Internal','Internal-External','External-Internal','Federated','Internal-Federated','External-Federated','Unknown'
         
        .PARAMETER SessionTypes
        Show sessions that match one or more session scenarios. Case sensitive. Choose one or more from:
        'Conference','Peer To Peer','Peer To Peer (Multimedia)','PSTN/External'
         
        .PARAMETER Codecs
        Show sessions where the selected codec was used by either caller or callee. Can query for multiple codecs. Case sensitive. Use Get-NectarCodecs for a list of valid codecs.
 
        .PARAMETER CallerCodecs
        Show sessions where the selected codec was used by the caller. Can query for multiple codecs. Case sensitive. Use Get-NectarCodecs for a list of valid codecs.
         
        .PARAMETER CalleeCodecs
        Show sessions where the selected codec was used by the callee. Can query for multiple codecs. Case sensitive. Use Get-NectarCodecs for a list of valid codecs.
         
        .PARAMETER Devices
        Show sessions where the selected device was used by either caller or callee. Can query for multiple devices. Case sensitive. Use Get-NectarSupportedDevice for a list of valid devices.
 
        .PARAMETER CallerDevices
        Show sessions where the selected device was used by the caller. Can query for multiple devices. Case sensitive. Use Get-NectarSupportedDevice for a list of valid devices.
         
        .PARAMETER CalleeDevices
        Show sessions where the selected device was used by the callee. Can query for multiple devices. Case sensitive. Use Get-NectarSupportedDevice for a list of valid devices.
 
        .PARAMETER CaptureDevices
        Show sessions where the selected capture device (microphone) was used by either caller or callee. Can query for multiple devices. Case sensitive. Use Get-NectarSupportedDevice for a list of valid devices.
 
        .PARAMETER CallerCaptureDevices
        Show sessions where the selected capture device (microphone) was used by the caller. Can query for multiple devices. Case sensitive. Use Get-NectarSupportedDevice for a list of valid devices.
         
        .PARAMETER CalleeCaptureDevices
        Show sessions where the selected capture device (microphone) was used by the callee. Can query for multiple devices. Case sensitive. Use Get-NectarSupportedDevice for a list of valid devices.
 
        .PARAMETER RenderDevices
        Show sessions where the selected render device (speaker) was used by either caller or callee. Can query for multiple devices. Case sensitive. Use Get-NectarSupportedDevice for a list of valid devices.
 
        .PARAMETER CallerRenderDevices
        Show sessions where the selected render device (speaker) was used by the caller. Can query for multiple devices. Case sensitive. Use Get-NectarSupportedDevice for a list of valid devices.
         
        .PARAMETER CalleeRenderDevices
        Show sessions where the selected render device (speaker) was used by the callee. Can query for multiple devices. Case sensitive. Use Get-NectarSupportedDevice for a list of valid devices.
 
        .PARAMETER DeviceVersions
        Show sessions where the selected device version was used by either caller or callee. Can query for multiple devices. Case sensitive. Use Get-NectarClientVersion for a list of valid client versions.
 
        .PARAMETER CallerDeviceVersions
        Show sessions where the selected device version was used by the caller. Can query for multiple devices. Case sensitive. Use Get-NectarClientVersion for a list of valid client versions.
         
        .PARAMETER CalleeDeviceVersions
        Show sessions where the selected device version was used by the callee. Can query for multiple devices. Case sensitive. Use Get-NectarClientVersion for a list of valid client versions.
 
        .PARAMETER IPAddresses
        Show sessions where the selected IP address was used by either caller or callee. Can query for multiple IPs.
 
        .PARAMETER CallerIPAddresses
        Show sessions where the selected IP address was used by the caller. Can query for multiple IPs.
         
        .PARAMETER CalleeIPAddresses
        Show sessions where the selected IP address was used by the callee. Can query for multiple IPs.
         
        .PARAMETER Locations
        Show sessions where the selected location was used by either caller or callee. Can query for multiple locations.
 
        .PARAMETER CallerLocations
        Show sessions where the selected location was used by the caller. Can query for multiple locations.
         
        .PARAMETER CalleeLocations
        Show sessions where the selected location was used by the callee. Can query for multiple locations.
                 
        .PARAMETER ExtCities
        Show sessions where the caller or callee was located in the selected city (as detected via geolocating the user's external IP address). Can query for multiple cities.
 
        .PARAMETER CallerExtCities
        Show sessions where the caller was located in the selected city (as detected via geolocating the user's external IP address). Can query for multiple cities.
         
        .PARAMETER CalleeExtCities
        Show sessions where the callee was located in the selected city (as detected via geolocating the user's external IP address). Can query for multiple cities.
         
        .PARAMETER ExtCountries
        Show sessions where the caller or callee was located in the selected country (as detected via geolocating the user's external IP address). Can query for multiple countries.
 
        .PARAMETER CallerExtCountries
        Show sessions where the caller was located in the selected country (as detected via geolocating the user's external IP address). Can query for multiple countries.
         
        .PARAMETER CalleeExtCountries
        Show sessions where the callee was located in the selected country (as detected via geolocating the user's external IP address). Can query for multiple countries.
         
        .PARAMETER ExtISPs
        Show sessions where the caller or callee was located in the selected ISP (as detected via geolocating the user's external IP address). Can query for multiple ISPs.
 
        .PARAMETER CallerExtISPs
        Show sessions where the caller was located in the selected ISP (as detected via geolocating the user's external IP address). Can query for multiple ISPs.
         
        .PARAMETER CalleeExtISPs
        Show sessions where the callee was located in the selected ISP (as detected via geolocating the user's external IP address). Can query for multiple ISPs.
         
        .PARAMETER NetworkTypes
        Show sessions where the selected network type was used by either caller or callee. Can query for multiple network types. Case sensitive. Choose one or more from:
        'Enterprise WiFi','Enterprise Wired','External Mobile','External WiFi','External Wired','Unknown','Unavailable'
 
        .PARAMETER CallerNetworkTypes
        Show sessions where the selected network type was used by the caller. Can query for multiple network types. Case sensitive. Choose one or more from:
        'Enterprise WiFi','Enterprise Wired','External Mobile','External WiFi','External Wired','Unknown','Unavailable'
         
        .PARAMETER CalleeNetworkTypes
        Show sessions where the selected network type was used by the callee. Can query for multiple network types. Case sensitive. Choose one or more from:
        'Enterprise WiFi','Enterprise Wired','External Mobile','External WiFi','External Wired','Unknown','Unavailable'
 
        .PARAMETER Platforms
        Show sessions where the selected platform was used by either caller or callee. Can query for multiple platforms. Case sensitive. Choose one or more from:
        'SKYPE','CISCO','CISCO_CMS','CISCO_VKM','TEAMS','SKYPE_ONLINE','CISCO_CMS_VKM','AVAYA_AURA_CM','RIG','LYNC_VKM','SKYPE_FOR_BUSINESS_VKM','CISCO_UNITY','CISCO_EXPRESSWAY','AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','ZOOM','ENDPOINT_CLIENT','MISCELLANEOUS'
 
        .PARAMETER CallerPlatforms
        Show sessions where the selected platform was used by the caller. Can query for multiple platforms. Case sensitive. Choose one or more from:
        'SKYPE','CISCO','CISCO_CMS','CISCO_VKM','TEAMS','SKYPE_ONLINE','CISCO_CMS_VKM','AVAYA_AURA_CM','RIG','LYNC_VKM','SKYPE_FOR_BUSINESS_VKM','CISCO_UNITY','CISCO_EXPRESSWAY','AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','ZOOM','ENDPOINT_CLIENT','MISCELLANEOUS'
         
        .PARAMETER CalleePlatforms
        Show sessions where the selected platform was used by the callee. Can query for multiple platforms. Case sensitive. Choose one or more from:
        'SKYPE','CISCO','CISCO_CMS','CISCO_VKM','TEAMS','SKYPE_ONLINE','CISCO_CMS_VKM','AVAYA_AURA_CM','RIG','LYNC_VKM','SKYPE_FOR_BUSINESS_VKM','CISCO_UNITY','CISCO_EXPRESSWAY','AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','ZOOM','ENDPOINT_CLIENT','MISCELLANEOUS'
 
        .PARAMETER Scenarios
        Show sessions where the selected scenario was used by either caller or callee. Can query for multiple scenarios. Choose one or more from:
        'External','Internal','Federated','Unknown'
 
        .PARAMETER CallerScenarios
        Show sessions where the selected scenario was used by the caller. Can query for multiple scenarios. Choose one or more from:
        'External','Internal','Federated','Unknown'
         
        .PARAMETER CalleeScenarios
        Show sessions where the selected scenario was used by the callee. Can query for multiple scenarios. Choose one or more from:
        'External','Internal','Federated','Unknown'
 
        .PARAMETER Subnets
        Show sessions where the selected subnet was used by either caller or callee. Can query for multiple subnets.
 
        .PARAMETER CallerSubnets
        Show sessions where the selected subnet was used by the caller. Can query for multiple subnets.
         
        .PARAMETER CalleeSubnets
        Show sessions where the selected subnet was used by the callee. Can query for multiple subnets.
         
        .PARAMETER Users
        Show sessions where the selected user was either caller or callee. Can query for multiple users.
 
        .PARAMETER FromUsers
        Show sessions where the selected user was the caller. Can query for multiple users.
         
        .PARAMETER ToUsers
        Show sessions where the selected user was the callee. Can query for multiple users.
         
        .PARAMETER ConfOrganizers
        Show sessions hosted by a specified conference organizer. Can query for multiple organizers.
         
        .PARAMETER VPN
        Show sessions where the selected VPN was used by either caller or callee.
 
        .PARAMETER CallerVPN
        Show sessions where the selected VPN was used by the caller.
         
        .PARAMETER CalleeVPN
        Show sessions where the selected VPN was used by the callee.
         
        .PARAMETER ParticipantsMinCount
        Show sessions where the number of participants is greater than or equal to the entered value
 
        .PARAMETER ParticipantsMaxCount
        Show sessions where the number of participants is less than or equal to the entered value
         
        .PARAMETER FeedbackRating
        Show sessions where users provided specific feedback ratings from BAD to EXCELLENT.
        Allowed values are BAD, POOR, FAIR, GOOD, EXCELLENT. Corresponds to ratings from 1 to 5 stars.
 
        .PARAMETER TenantName
        The name of the Nectar 10 tenant. Used in multi-tenant configurations.
         
        .EXAMPLE
        Get-NectarSessionHistograms -TimePeriod LAST_HOUR
        Returns a minute-by-minute count of the number of sessions occuring over the past hour
 
        .NOTES
        Version 1.0
    #>

    
    [Alias("gnsh")]
    Param (
        [parameter(Mandatory=$False)]
        [ValidateSet('LAST_HOUR','LAST_DAY','LAST_WEEK','LAST_MONTH','CUSTOM', IgnoreCase=$True)]
        [string]$TimePeriod = 'LAST_HOUR',
        [parameter(Mandatory=$False)]
        [Alias("StartDateFrom")]
        [string]$TimePeriodFrom,
        [parameter(Mandatory=$False)]
        [Alias("StartDateTo")]
        [string]$TimePeriodTo,
        [parameter(Mandatory=$False)]
        [ValidateSet('GOOD','POOR_0_25','PARTIALLY_GOOD_25_50','PARTIALLY_GOOD_50_75','PARTIALLY_GOOD_75_100','UNAVAILABLE','UNKNOWN', IgnoreCase=$True)]
        [string[]]$SessionQualities,
        [Parameter(Mandatory=$False)]
        [ValidateRange(0,99999999)]
        [int]$DurationFrom = 0,
        [Parameter(Mandatory=$False)]
        [ValidateRange(0,99999999)]
        [int]$DurationTo = 99999999,
        [parameter(Mandatory=$False)]
        [ValidateSet('AUDIO','VIDEO','APP_SHARING','FILE_TRANSFER','IM','UNKNOWN','TOTAL','VBSS','REMOTE_ASSISTANCE','APP_INVITE','FOCUS','UNKNOWN', IgnoreCase=$True)]
        [string[]]$Modalities = 'TOTAL',
        [parameter(Mandatory=$False)]
        [ValidateSet('TCP','UDP','Unknown', IgnoreCase=$False)]
        [string[]]$Protocols,
        [Parameter(Mandatory=$False)]
        [ValidateRange(200,699)]
        [string[]]$ResponseCodes,
        [parameter(Mandatory=$False)]
        [ValidateSet('INTERNAL','EXTERNAL','FEDERATED','INTERNAL_EXTERNAL','EXTERNAL_INTERNAL','FEDERATED_INTERNAL','INTERNAL_FEDERATED','FEDERATED_EXTERNAL','EXTERNAL_FEDERATED','UNKNOWN', IgnoreCase=$True)]
        [string[]]$SessionScenarios,
        [parameter(Mandatory=$False)]
        [ValidateSet('CONFERENCE','CONFERENCE_SESSION','PEER2PEER','PEER2PEER_MULTIMEDIA','PSTN', IgnoreCase=$False)]
        [string[]]$SessionTypes,
        [parameter(Mandatory=$False)]
        [string[]]$Codecs,
        [parameter(Mandatory=$False)]
        [string[]]$CallerCodecs,
        [parameter(Mandatory=$False)]
        [string[]]$CalleeCodecs,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$Devices,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerDevices,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeDevices,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CaptureDevices,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerCaptureDevices,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeCaptureDevices,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$RenderDevices,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerRenderDevices,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeRenderDevices,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$DeviceVersions,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerDeviceVersions,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeDeviceVersions,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ipaddress[]]$IPAddresses,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ipaddress[]]$CallerIPAddresses,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ipaddress[]]$CalleeIPAddresses,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$Locations,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerLocations,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeLocations,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ExtCities,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerExtCities,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeExtCities,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ExtCountries,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerExtCountries,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeExtCountries,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ExtISPs,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerExtISPs,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeExtISPs,
        [parameter(Mandatory=$False)]
        [ValidateSet('Enterprise WiFi','Enterprise Wired','External Mobile','External WiFi','External Wired','Unknown','Unavailable', IgnoreCase=$False)]
        [string[]]$NetworkTypes,
        [parameter(Mandatory=$False)]
        [ValidateSet('Enterprise WiFi','Enterprise Wired','External Mobile','External WiFi','External Wired','Unknown','Unavailable', IgnoreCase=$False)]
        [string[]]$CallerNetworkTypes,
        [parameter(Mandatory=$False)]
        [ValidateSet('Enterprise WiFi','Enterprise Wired','External Mobile','External WiFi','External Wired','Unknown','Unavailable', IgnoreCase=$False)]
        [string[]]$CalleeNetworkTypes,
        [parameter(Mandatory=$False)]
        [ValidateSet('SKYPE','CISCO','CISCO_CMS','CISCO_VKM','TEAMS','SKYPE_ONLINE','CISCO_CMS_VKM','AVAYA_AURA_CM','RIG','LYNC_VKM','SKYPE_FOR_BUSINESS_VKM','CISCO_UNITY','CISCO_EXPRESSWAY','AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','ZOOM','ENDPOINT_CLIENT','MISCELLANEOUS', IgnoreCase=$True)]
        [string[]]$Platforms,
        [parameter(Mandatory=$False)]
        [ValidateSet('SKYPE','CISCO','CISCO_CMS','CISCO_VKM','TEAMS','SKYPE_ONLINE','CISCO_CMS_VKM','AVAYA_AURA_CM','RIG','LYNC_VKM','SKYPE_FOR_BUSINESS_VKM','CISCO_UNITY','CISCO_EXPRESSWAY','AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','ZOOM','ENDPOINT_CLIENT','MISCELLANEOUS', IgnoreCase=$True)]
        [string[]]$CallerPlatforms,
        [parameter(Mandatory=$False)]
        [ValidateSet('SKYPE','CISCO','CISCO_CMS','CISCO_VKM','TEAMS','SKYPE_ONLINE','CISCO_CMS_VKM','AVAYA_AURA_CM','RIG','LYNC_VKM','SKYPE_FOR_BUSINESS_VKM','CISCO_UNITY','CISCO_EXPRESSWAY','AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','ZOOM','ENDPOINT_CLIENT','MISCELLANEOUS', IgnoreCase=$True)]
        [string[]]$CalleePlatforms,
        [parameter(Mandatory=$False)]
        [ValidateSet('External','Internal','Federated','Unknown', IgnoreCase=$True)]
        [string[]]$Scenarios,
        [parameter(Mandatory=$False)]
        [ValidateSet('External','Internal','Federated','Unknown', IgnoreCase=$True)]
        [string[]]$CallerScenarios,
        [parameter(Mandatory=$False)]
        [ValidateSet('External','Internal','Federated','Unknown', IgnoreCase=$True)]
        [string[]]$CalleeScenarios,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ipaddress[]]$Subnets,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ipaddress[]]$CallerSubnets,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ipaddress[]]$CalleeSubnets,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$Users,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$FromUsers,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ToUsers,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ConfOrganizers,
        [parameter(Mandatory=$False)]
        [ValidateSet('TRUE','FALSE','UNKNOWN', IgnoreCase=$True)]
        [string[]]$VPN,
        [parameter(Mandatory=$False)]
        [ValidateSet('TRUE','FALSE','UNKNOWN', IgnoreCase=$True)]
        [string[]]$CallerVPN,
        [parameter(Mandatory=$False)]
        [ValidateSet('TRUE','FALSE','UNKNOWN', IgnoreCase=$True)]
        [string[]]$CalleeVPN,
        [parameter(Mandatory=$False)]
        [ValidateRange(0,99999)]
        [int]$ParticipantsMinCount,
        [parameter(Mandatory=$False)]
        [ValidateRange(0,99999)]
        [int]$ParticipantsMaxCount,
        [parameter(Mandatory=$False)]
        [ValidateSet('BAD','POOR','FAIR','GOOD','EXCELLENT', IgnoreCase=$False)]
        [string[]]$FeedbackRating,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
            
            $Params = @{}
            
            If ($Modalities) { $Modalities | %{$ModalitiesStr += ($(if($ModalitiesStr){","}) + $_)}; $Params.Add('Modalities',$ModalitiesStr) }
            
            If ($TenantName) { $Params.Add('Tenant',$TenantName) }
            
            #Remove Modalities from FilterSession POST. For some reason, TOTAL results come back as empty if this is set
            $PSBoundParameters.Remove('Modalities') | Out-Null
            $FilterSession = Set-NectarFilterParams @PsBoundParameters

            $URI = "https://$Global:NectarCloud/dapi/session/histograms"
            
            Write-Verbose $URI

            $JSON = Invoke-RestMethod -Method GET -uri $URI -Body $Params -WebSession $FilterSession
            Return $JSON
        }
        Catch {
            Write-Error 'Session histogram not found.'
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}




Function Get-NectarSessionSummary {
    <#
        .SYNOPSIS
        Returns call summary information for a given session
         
        .DESCRIPTION
        Returns call summary information for a given session. This is used to populate the top few sections of an individual session on the session OVERVIEW screen.
        UI_ELEMENT
 
        .PARAMETER SessionID
        The session ID of the selected session
         
        .PARAMETER Platform
        The platform where the session took place
 
        .PARAMETER TenantName
        The name of the Nectar 10 tenant. Used in multi-tenant configurations.
         
        .EXAMPLE
        Get-NectarSessionSummary 2021-04-30T16:04:28.572701_1_1_*_*_*_6_*_29fe15a4-99e5-4a2c-92a6-fbf3024944fc_29abe23a4-33e5-4a2c-92a6-faf30445e5bc_* -Platform TEAMS
        Returns summary information for a specific Teams session
         
        .EXAMPLE
        Get-NectarSessions -Platform TEAMS -Users tferguson@contoso.com -SessionTypes PEER2PEER -TimePeriod LAST_DAY | Get-NectarSessionSummary -Platform TEAMS
        Returns summary information for all Teams peer-to-peer calls for TFerguson for the last day.
         
        .NOTES
        Version 1.1
    #>

    
    [Alias("gnss")]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [string]$SessionID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [Alias('callerPlatform', 'calleePlatform', 'Platforms')]
        [ValidateSet('SKYPE','CISCO','CISCO_CMS','CISCO_VKM','TEAMS','SKYPE_ONLINE','CISCO_CMS_VKM','AVAYA_AURA_CM','RIG','LYNC_VKM','SKYPE_FOR_BUSINESS_VKM','CISCO_UNITY','CISCO_EXPRESSWAY','AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','ZOOM','ENDPOINT_CLIENT','MISCELLANEOUS', IgnoreCase=$True)]
        [string[]]$Platform,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
        
            $Platform = $Platform.ToUpper()
            $JSON = Invoke-RestMethod -Method GET -Credential $Global:NectarCred -uri "https://$Global:NectarCloud/dapi/session/$SessionID/summary?platform=$Platform&tenant=$TenantName"
            
            $SessionSummary = [pscustomobject][ordered]@{
                SessionID = $SessionID
                StartTime = $JSON.startTime
                EndTime = $JSON.endTime
                Duration = $JSON.duration
                Quality = $JSON.quality
                CallerRenderDevice = $JSON.caller.renderDevice.value
                CallerCaptureDevice = $JSON.caller.captureDevice.value
                CallerClientVersion = $JSON.caller.clientVersion.value
                CallerNetworkType = $JSON.caller.networkType.value
                CallerNetworkWarning = $JSON.caller.networkType.warning
                CallerServer = $JSON.caller.server.value
                CallerServerAlertLevel = $JSON.caller.server.alertLevel
                CalleeRenderDevice = $JSON.callee.renderDevice.value
                CalleeCaptureDevice = $JSON.callee.captureDevice.value
                CalleeClientVersion = $JSON.callee.clientVersion.value
                CalleeNetworkType = $JSON.callee.networkType.value
                CalleeNetworkWarning = $JSON.callee.networkType.warning
                CalleeServer = $JSON.callee.server.value
                CalleeServerAlertLevel = $JSON.callee.server.alertLevel
            }

            Return $SessionSummary
        }
        Catch {
            Write-Error 'Session diagnostics not found.'
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



Function Get-NectarSessionDetails {
    <#
        .SYNOPSIS
        Returns details for a given session
         
        .DESCRIPTION
        Returns details for a given session. This is used to populate the session ADVANCED screen for a given session.
        UI_ELEMENT
         
        .PARAMETER SessionID
        The session ID of the selected session
         
        .PARAMETER Platform
        The platform where the session took place
 
        .PARAMETER TenantName
        The name of the Nectar 10 tenant. Used in multi-tenant configurations.
         
        .EXAMPLE
        Get-NectarSessionDetails 2021-04-30T16:04:28.572701_1_1_*_*_*_6_*_29fe15a4-99e5-4a2c-92a6-fbf3024944fc_29abe23a4-33e5-4a2c-92a6-faf30445e5bc_* -Platform TEAMS
        Returns detailed information for a specific Teams session
         
        .EXAMPLE
        Get-NectarSessions -Platform TEAMS -Users tferguson@contoso.com -SessionTypes PEER2PEER,PEER2PEER_MULTIMEDIA -TimePeriod LAST_DAY | Get-NectarSessionDetails -Platform TEAMS
        Returns detailed information for all Teams peer-to-peer and multimedia P2P calls for TFerguson for the last day.
 
        .NOTES
        Version 1.2
    #>

    [Alias("gnsd")]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [Alias("ID")]
        [string]$SessionID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [Alias('callerPlatform', 'calleePlatform', 'Platforms')]
        [ValidateSet('SKYPE','CISCO','CISCO_CMS','CISCO_VKM','TEAMS','SKYPE_ONLINE','CISCO_CMS_VKM','AVAYA_AURA_CM','RIG','LYNC_VKM','SKYPE_FOR_BUSINESS_VKM','CISCO_UNITY','CISCO_EXPRESSWAY','AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','ZOOM','ENDPOINT_CLIENT','MISCELLANEOUS', IgnoreCase=$True)]
        [string[]]$Platform,
        [Parameter(ValueFromPipelineByPropertyName=$True, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
            
            $Platform = $Platform.ToUpper()
            $JSON = Invoke-RestMethod -Method GET -Credential $Global:NectarCred -uri "https://$Global:NectarCloud/dapi/session/$SessionID/advanced?platform=$Platform&tenant=$TenantName"
            
            $SessionDetails = [pscustomobject][ordered]@{
                SessionID = $SessionID
                SessionType = $JSON.type
                Caller = $JSON.caller
                Callee = $JSON.callee
            }

            ForEach ($DataGroup in $JSON.groups) {
                ForEach ($DataElement in $DataGroup.data.PsObject.Properties) {
                    $SessionDetails | Add-Member -NotePropertyName $DataElement.Name -NotePropertyValue $DataElement.Value.Value
                }
            }

            Return $SessionDetails
        }
        Catch {
            Write-Error 'Session details not found.'
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



Function Get-NectarSessionAlerts {
    <#
        .SYNOPSIS
        Returns alerts for a given session
         
        .DESCRIPTION
        Returns alerts for a given session. This is used to populate the SESSION ALERTS portion of the session OVERVIEW screen.
        UI_ELEMENT
                 
        .PARAMETER SessionID
        The session ID of the selected session
         
        .PARAMETER Platform
        The platform where the session took place
 
        .PARAMETER TenantName
        The name of the Nectar 10 tenant. Used in multi-tenant configurations.
         
        .EXAMPLE
        Get-NectarSessionAlerts 2021-04-30T16:04:28.572701_1_1_*_*_*_6_*_29fe15a4-99e5-4a2c-92a6-fbf3024944fc_29abe23a4-33e5-4a2c-92a6-faf30445e5bc_* -Platform TEAMS
        Returns alert information for a specific Teams session
         
        .EXAMPLE
        Get-NectarSessions -Platform TEAMS -Users tferguson@contoso.com -SessionTypes PEER2PEER -TimePeriod LAST_DAY | Get-NectarSessionAlerts -Platform TEAMS
        Returns session alerts for all Teams peer-to-peer calls for TFerguson for the last day.
 
        .NOTES
        Version 1.2
    #>

    
    [Alias("gnsa")]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [string]$SessionID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [Alias('callerPlatform', 'calleePlatform', 'Platforms')]
        [ValidateSet('SKYPE','CISCO','CISCO_CMS','CISCO_VKM','TEAMS','SKYPE_ONLINE','CISCO_CMS_VKM','AVAYA_AURA_CM','RIG','LYNC_VKM','SKYPE_FOR_BUSINESS_VKM','CISCO_UNITY','CISCO_EXPRESSWAY','AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','ZOOM','ENDPOINT_CLIENT','MISCELLANEOUS', IgnoreCase=$True)]
        [string[]]$Platform,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
            
            $Platform = $Platform.ToUpper()
            $JSON = Invoke-RestMethod -Method GET -Credential $Global:NectarCred -uri "https://$Global:NectarCloud/dapi/session/$SessionID/alerts?platform=$Platform&tenant=$TenantName"
            
            $UserList = 'Caller','Callee'
            
            ForEach ($User in $UserList) {    
                ForEach ($Alert in $JSON.$User.alerts.PsObject.Properties) {
                    $AlertDetails = [pscustomobject][ordered]@{
                        SessionID = $SessionID
                        User = $User
                        Parameter = $Alert.Name
                        Value = $Alert.Value.Value
                        AlertLevel = $Alert.Value.AlertLevel
                    }
                    $AlertDetails
                }
            }
        }
        Catch {
            Write-Error 'Session alerts not found.'
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}




Function Get-NectarSessionDiagnostics {
    <#
        .SYNOPSIS
        Returns call diagnostics for a given session
         
        .DESCRIPTION
        Returns call diagnostics for a given session. This is used to populate the session DIAGNOSTICS screen for a given session.
        UI_ELEMENT
         
        .PARAMETER SessionID
        The session ID of the selected session
         
        .PARAMETER Platform
        The platform where the session took place
 
        .PARAMETER TenantName
        The name of the Nectar 10 tenant. Used in multi-tenant configurations.
         
        .EXAMPLE
        Get-NectarSessionDetails 2021-04-30T16:04:28.572701_1_1_*_*_*_6_*_29fe15a4-99e5-4a2c-92a6-fbf3024944fc_29abe23a4-33e5-4a2c-92a6-faf30445e5bc_* -Platform SKYPE
        Returns diagnostic information for a specific Skype for Business session
         
        .EXAMPLE
        Get-NectarSessions -Platform SKYPE -Users tferguson@contoso.com -SessionTypes PEER2PEER -TimePeriod LAST_DAY | Get-NectarSessionDetails -Platform SKYPE
        Returns detailed information for all Skype for Business peer-to-peer calls for TFerguson for the last day.
 
        .NOTES
        Version 1.2
    #>

    
    [Alias("gnsd")]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [Alias("id")]
        [string]$SessionID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [Alias('callerPlatform', 'calleePlatform', 'Platforms')]
        [ValidateSet('SKYPE','CISCO','CISCO_CMS','CISCO_VKM','TEAMS','SKYPE_ONLINE','CISCO_CMS_VKM','AVAYA_AURA_CM','RIG','LYNC_VKM','SKYPE_FOR_BUSINESS_VKM','CISCO_UNITY','CISCO_EXPRESSWAY','AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','ZOOM','ENDPOINT_CLIENT','MISCELLANEOUS', IgnoreCase=$True)]
        [string[]]$Platform,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
            
            $Platform = $Platform.ToUpper()
            $JSON = Invoke-RestMethod -Method GET -Credential $Global:NectarCred -uri "https://$Global:NectarCloud/dapi/session/$SessionID/diagnostic?platform=$Platform&tenant=$TenantName"
            $JSON.elements | Add-Member -NotePropertyName 'sessionId' -NotePropertyValue $SessionID
            Return $JSON.elements
        }
        Catch {
            Write-Error 'Session diagnostics not found.'
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}




Function Get-NectarConferenceSummary {
    <#
        .SYNOPSIS
        Returns session summaries for a given conference
         
        .DESCRIPTION
        Returns session summaries for a given conference. This is used to populate the CONFERENCE section of the details of a specific conference.
        UI_ELEMENT
         
        .PARAMETER ConferenceID
        The conference ID of the selected conference
         
        .PARAMETER Platform
        The platform where the conference took place
 
        .PARAMETER TenantName
        The name of the Nectar 10 tenant. Used in multi-tenant configurations.
 
        .EXAMPLE
        Get-NectarConferenceSummary 2021-05-06T13:30:34.795296_*_*_*_*_*_*_173374c1-a15a-47dd-b11c-d32ab5442774_*_*
        Returns conference summary information for a specific conference
         
        .EXAMPLE
        Get-NectarSessions -TimePeriod LAST_DAY -Users tferguson@contoso.com-SessionTypes CONFERENCE -Platforms TEAMS | Get-NectarConferenceSummary -Platform TEAMS
        Returns the conference summary information for all Teams conferences participated by tferguson@contoso.com for the past day
         
        .NOTES
        Version 1.2
    #>

    
    [Alias("gncs")]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [Alias("id")]
        [string]$ConferenceID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [Alias('callerPlatform', 'calleePlatform', 'Platforms')]
        [ValidateSet('SKYPE','CISCO','CISCO_CMS','CISCO_VKM','TEAMS','SKYPE_ONLINE','CISCO_CMS_VKM','AVAYA_AURA_CM','RIG','LYNC_VKM','SKYPE_FOR_BUSINESS_VKM','CISCO_UNITY','CISCO_EXPRESSWAY','AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','ZOOM','ENDPOINT_CLIENT','MISCELLANEOUS', IgnoreCase=$True)]
        [string[]]$Platform,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
            
            $Platform = $Platform.ToUpper()
            $JSON = Invoke-RestMethod -Method GET -Credential $Global:NectarCred -uri "https://$Global:NectarCloud/dapi/conference/$ConferenceID/?platform=$Platform&tenant=$TenantName"
            
            $ConferenceSummary = [pscustomobject][ordered]@{
                ConferenceID = $ConferenceID
                StartTime = $JSON.conference.startTime
                EndTime = $JSON.conference.endTime
                Duration = $JSON.duration
                AverageMOS = $JSON.avgMos
                Participants = $JSON.participants
                TotalSessions = $JSON.sessions.total
                GoodSessions = $JSON.sessions.good
                PoorSessions = $JSON.sessions.poor
                UnknownSessions = $JSON.sessions.unknown
            }

            Return $ConferenceSummary
        }
        Catch {
            Write-Error 'Conference not found.'
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



Function Get-NectarConferenceTimeline {
    <#
        .SYNOPSIS
        Returns session timeline details for a given conference
         
        .DESCRIPTION
        Returns session timeline details for a given conference. This is used to build the Gantt chart view of a specific conference.
        UI_ELEMENT
         
        .PARAMETER ConferenceID
        The conference ID of the selected conference
         
        .PARAMETER Platform
        The platform where the conference took place
 
        .PARAMETER TenantName
        The name of the Nectar 10 tenant. Used in multi-tenant configurations.
 
        .EXAMPLE
        Get-NectarConferenceTimeline 2021-05-06T13:30:34.795296_*_*_*_*_*_*_173374c1-a15a-47dd-b11c-d32ab5442774_*_* -Platform TEAMS
        Returns conference summary information for a specific conference
         
        .EXAMPLE
        Get-NectarSessions -TimePeriod LAST_DAY -Users tferguson@contoso.com-SessionTypes CONFERENCE -Platforms TEAMS | Get-NectarConferenceTimeline -Platform TEAMS
        Returns the conference timeline information for all Teams conferences participated by tferguson@contoso.com for the past day
         
        .NOTES
        Version 1.2
    #>

    
    [Alias("gnct")]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [Alias("id")]
        [string]$ConferenceID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [Alias('callerPlatform', 'calleePlatform', 'Platforms')]
        [ValidateSet('SKYPE','CISCO','CISCO_CMS','CISCO_VKM','TEAMS','SKYPE_ONLINE','CISCO_CMS_VKM','AVAYA_AURA_CM','RIG','LYNC_VKM','SKYPE_FOR_BUSINESS_VKM','CISCO_UNITY','CISCO_EXPRESSWAY','AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','ZOOM','ENDPOINT_CLIENT','MISCELLANEOUS', IgnoreCase=$True)]
        [string[]]$Platform,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
            
            $Platform = $Platform.ToUpper()
            $JSON = Invoke-RestMethod -Method GET -Credential $Global:NectarCred -uri "https://$Global:NectarCloud/dapi/conference/$ConferenceID/timeline?platform=$Platform&tenant=$TenantName"
            $JSON | Add-Member -NotePropertyName 'conferenceId' -NotePropertyValue $ConferenceID
            $JSON | Add-Member -TypeName 'Nectar.Conference.Timeline'
            Return $JSON
        }
        Catch {
            Write-Error 'Conference not found.'
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



Function Get-NectarConferenceParticipants {
    <#
        .SYNOPSIS
        Returns session participant details for a given conference
         
        .DESCRIPTION
        Returns session participant details for a given conference. This is used to build the PARTICIPANTS section of a specific conference.
        UI_ELEMENT
                 
        .PARAMETER ConferenceID
        The conference ID of the selected conference
         
        .PARAMETER Platform
        The platform where the conference took place
 
        .PARAMETER TenantName
        The name of the Nectar 10 tenant. Used in multi-tenant configurations.
 
        .EXAMPLE
        Get-NectarConferenceParticipants 2021-05-06T13:30:34.795296_*_*_*_*_*_*_173374c1-a15a-47dd-b11c-d32ab5442774_*_* -Platform TEAMS
        Returns conference participant information for a specific conference
         
        .EXAMPLE
        Get-NectarSessions -TimePeriod LAST_DAY -Users tferguson@contoso.com-SessionTypes CONFERENCE -Platforms TEAMS | Get-NectarConferenceParticipants -Platform TEAMS
        Returns the conference participant information for all Teams conferences participated by tferguson@contoso.com for the past day
         
        .NOTES
        Version 1.1
    #>

    
    [Alias("gncp")]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [Alias("id")]
        [string]$ConferenceID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [Alias('callerPlatform', 'calleePlatform', 'Platforms')]
        [ValidateSet('SKYPE','CISCO','CISCO_CMS','CISCO_VKM','TEAMS','SKYPE_ONLINE','CISCO_CMS_VKM','AVAYA_AURA_CM','RIG','LYNC_VKM','SKYPE_FOR_BUSINESS_VKM','CISCO_UNITY','CISCO_EXPRESSWAY','AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','ZOOM','ENDPOINT_CLIENT','MISCELLANEOUS', IgnoreCase=$True)]
        [string[]]$Platform,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
            
            $Platform = $Platform.ToUpper()
            $JSON = Invoke-RestMethod -Method GET -Credential $Global:NectarCred -uri "https://$Global:NectarCloud/dapi/conference/$ConferenceID/participants?platform=$Platform&tenant=$TenantName"
            $JSON | Add-Member -NotePropertyName 'conferenceId' -NotePropertyValue $ConferenceID
            $JSON | Add-Member -TypeName 'Nectar.Conference.Participants'
            Return $JSON
        }
        Catch {
            Write-Error 'Conference not found.'
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}




Function Get-NectarSessionMultiTimeline {
    <#
        .SYNOPSIS
        Returns multimedia session timeline details for a given P2P multimedia session
         
        .DESCRIPTION
        Returns multimedia session (an audio/video P2P session or an audio/appsharing P2P session) timeline details for a given session
        Used to build the Gantt chart view for a given P2P multimedia session on the Session Details screen.
        UI_ELEMENT
                 
        .PARAMETER SessionID
        The sessionID of a multimedia P2P session
         
        .PARAMETER Platform
        The platform where the P2P session took place
 
        .PARAMETER TenantName
        The name of the Nectar 10 tenant. Used in multi-tenant configurations.
 
        .EXAMPLE
        Get-NectarSessionMultiTimeline 2021-05-05T19:17:14.324027_1_1_*_*_*_6_*_6efc12345-4229-4c11-9001-9a00667a761e9_6efc8348-4809-4caf-9141-9afa87a761e9_* -Platform TEAMS
        Returns multimedia session timeline information for a specific multimedia P2P session
         
        .EXAMPLE
        Get-NectarSessions -TimePeriod LAST_DAY -Users tferguson@contoso.com-SessionTypes PEER2PEER_MULTIMEDIA -Platforms TEAMS | Get-NectarSessionMultiTimeline -Platform TEAMS
        Returns the multimedia session timeline information for all Teams P2P multimedia sessions participated by tferguson@contoso.com for the past day
         
        .NOTES
        Version 1.3
    #>

    
    [Alias("gnsmt")]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [string]$SessionID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [Alias('callerPlatform', 'calleePlatform', 'Platforms')]
        [ValidateSet('SKYPE','CISCO','CISCO_CMS','CISCO_VKM','TEAMS','SKYPE_ONLINE','CISCO_CMS_VKM','AVAYA_AURA_CM','RIG','LYNC_VKM','SKYPE_FOR_BUSINESS_VKM','CISCO_UNITY','CISCO_EXPRESSWAY','AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','ZOOM','ENDPOINT_CLIENT','MISCELLANEOUS', IgnoreCase=$True)]
        [string[]]$Platform,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
            
            $JSON = Invoke-RestMethod -Method GET -Credential $Global:NectarCred -uri "https://$Global:NectarCloud/dapi/session/$SessionID/multi/timeline?platform=$Platform&tenant=$TenantName"
            #$JSON | Add-Member -NotePropertyName 'sessionId' -NotePropertyValue $SessionID
            Return $JSON
        }
        Catch {
            Write-Error 'Multimedia session not found.'
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



Function Get-NectarModalityQualitySummary {
    <#
        .SYNOPSIS
        Returns summary quality information for different modalities over a given timeperiod.
 
        .DESCRIPTION
        Returns summary quality information for audio, video and appsharing modalities. Used to build the QUALITY section of the CALL DETAILS screen.
        UI_ELEMENT
         
        .PARAMETER Modality
        Show sessions that match one or more modality types. Not case sensitive. Choose one or more from:
        'AUDIO','VIDEO','APPSHARING'
         
        .PARAMETER TimePeriod
        The time period to show session data from. Select from 'LAST_HOUR','LAST_DAY','LAST_WEEK','LAST_MONTH','CUSTOM'.
        CUSTOM requires using TimePeriodFrom and TimePeriodTo parameters.
         
        .PARAMETER TimePeriodFrom
        The earliest date/time to show session data from. Must be used in conjunction with -TimePeriod CUSTOM and TimePeriodTo parameters. Use format 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'. All time/dates in UTC.
         
        .PARAMETER TimePeriodTo
        The latest date/time to show session data from. Must be used in conjunction with -TimePeriod CUSTOM and TimePeriodFrom parameters. Use format 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'. All time/dates in UTC.
         
        .PARAMETER SessionQualities
        Show sessions that match a given quality rating. Case sensitive. Choose one or more from:
        'GOOD','POOR_0_25','PARTIALLY_GOOD_25_50','PARTIALLY_GOOD_50_75','PARTIALLY_GOOD_75_100','UNAVAILABLE','UNKNOWN'
         
        .PARAMETER DurationFrom
        The shortest call length (in seconds) to show session data.
         
        .PARAMETER DurationTo
        The longest call length (in seconds) to show session data.
         
        .PARAMETER Protocols
        Show sessions that match one or more network protocol types. Case sensitive. Choose one or more from:
        'TCP','UDP','Unknown'
         
        .PARAMETER ResponseCodes
        Show sessions that match one or more SIP response codes. Accepts numbers from 200 to 699
         
        .PARAMETER SessionScenarios
        Show sessions that match one or more session scenarios. Not case sensitive. Choose one or more from:
        'External','Internal','Internal-External','External-Internal','Federated','Internal-Federated','External-Federated','Unknown'
         
        .PARAMETER SessionTypes
        Show sessions that match one or more session scenarios. Case sensitive. Choose one or more from:
        'Conference','Peer To Peer','Peer To Peer (Multimedia)','PSTN/External'
         
        .PARAMETER Codecs
        Show sessions where the selected codec was used by either caller or callee. Can query for multiple codecs. Case sensitive. Use Get-NectarCodecs for a list of valid codecs.
 
        .PARAMETER CallerCodecs
        Show sessions where the selected codec was used by the caller. Can query for multiple codecs. Case sensitive. Use Get-NectarCodecs for a list of valid codecs.
         
        .PARAMETER CalleeCodecs
        Show sessions where the selected codec was used by the callee. Can query for multiple codecs. Case sensitive. Use Get-NectarCodecs for a list of valid codecs.
         
        .PARAMETER Devices
        Show sessions where the selected device was used by either caller or callee. Can query for multiple devices. Case sensitive. Use Get-NectarSupportedDevice for a list of valid devices.
 
        .PARAMETER CallerDevices
        Show sessions where the selected device was used by the caller. Can query for multiple devices. Case sensitive. Use Get-NectarSupportedDevice for a list of valid devices.
         
        .PARAMETER CalleeDevices
        Show sessions where the selected device was used by the callee. Can query for multiple devices. Case sensitive. Use Get-NectarSupportedDevice for a list of valid devices.
 
        .PARAMETER CaptureDevices
        Show sessions where the selected capture device (microphone) was used by either caller or callee. Can query for multiple devices. Case sensitive. Use Get-NectarSupportedDevice for a list of valid devices.
 
        .PARAMETER CallerCaptureDevices
        Show sessions where the selected capture device (microphone) was used by the caller. Can query for multiple devices. Case sensitive. Use Get-NectarSupportedDevice for a list of valid devices.
         
        .PARAMETER CalleeCaptureDevices
        Show sessions where the selected capture device (microphone) was used by the callee. Can query for multiple devices. Case sensitive. Use Get-NectarSupportedDevice for a list of valid devices.
 
        .PARAMETER RenderDevices
        Show sessions where the selected render device (speaker) was used by either caller or callee. Can query for multiple devices. Case sensitive. Use Get-NectarSupportedDevice for a list of valid devices.
 
        .PARAMETER CallerRenderDevices
        Show sessions where the selected render device (speaker) was used by the caller. Can query for multiple devices. Case sensitive. Use Get-NectarSupportedDevice for a list of valid devices.
         
        .PARAMETER CalleeRenderDevices
        Show sessions where the selected render device (speaker) was used by the callee. Can query for multiple devices. Case sensitive. Use Get-NectarSupportedDevice for a list of valid devices.
 
        .PARAMETER DeviceVersions
        Show sessions where the selected device version was used by either caller or callee. Can query for multiple devices. Case sensitive. Use Get-NectarClientVersion for a list of valid client versions.
 
        .PARAMETER CallerDeviceVersions
        Show sessions where the selected device version was used by the caller. Can query for multiple devices. Case sensitive. Use Get-NectarClientVersion for a list of valid client versions.
         
        .PARAMETER CalleeDeviceVersions
        Show sessions where the selected device version was used by the callee. Can query for multiple devices. Case sensitive. Use Get-NectarClientVersion for a list of valid client versions.
 
        .PARAMETER IPAddresses
        Show sessions where the selected IP address was used by either caller or callee. Can query for multiple IPs.
 
        .PARAMETER CallerIPAddresses
        Show sessions where the selected IP address was used by the caller. Can query for multiple IPs.
         
        .PARAMETER CalleeIPAddresses
        Show sessions where the selected IP address was used by the callee. Can query for multiple IPs.
         
        .PARAMETER Locations
        Show sessions where the selected location was used by either caller or callee. Can query for multiple locations.
 
        .PARAMETER CallerLocations
        Show sessions where the selected location was used by the caller. Can query for multiple locations.
         
        .PARAMETER CalleeLocations
        Show sessions where the selected location was used by the callee. Can query for multiple locations.
                 
        .PARAMETER ExtCities
        Show sessions where the caller or callee was located in the selected city (as detected via geolocating the user's external IP address). Can query for multiple cities.
 
        .PARAMETER CallerExtCities
        Show sessions where the caller was located in the selected city (as detected via geolocating the user's external IP address). Can query for multiple cities.
         
        .PARAMETER CalleeExtCities
        Show sessions where the callee was located in the selected city (as detected via geolocating the user's external IP address). Can query for multiple cities.
         
        .PARAMETER ExtCountries
        Show sessions where the caller or callee was located in the selected country (as detected via geolocating the user's external IP address). Can query for multiple countries.
 
        .PARAMETER CallerExtCountries
        Show sessions where the caller was located in the selected country (as detected via geolocating the user's external IP address). Can query for multiple countries.
         
        .PARAMETER CalleeExtCountries
        Show sessions where the callee was located in the selected country (as detected via geolocating the user's external IP address). Can query for multiple countries.
         
        .PARAMETER ExtISPs
        Show sessions where the caller or callee was located in the selected ISP (as detected via geolocating the user's external IP address). Can query for multiple ISPs.
 
        .PARAMETER CallerExtISPs
        Show sessions where the caller was located in the selected ISP (as detected via geolocating the user's external IP address). Can query for multiple ISPs.
         
        .PARAMETER CalleeExtISPs
        Show sessions where the callee was located in the selected ISP (as detected via geolocating the user's external IP address). Can query for multiple ISPs.
         
        .PARAMETER NetworkTypes
        Show sessions where the selected network type was used by either caller or callee. Can query for multiple network types. Case sensitive. Choose one or more from:
        'Enterprise WiFi','Enterprise Wired','External Mobile','External WiFi','External Wired','Unknown','Unavailable'
 
        .PARAMETER CallerNetworkTypes
        Show sessions where the selected network type was used by the caller. Can query for multiple network types. Case sensitive. Choose one or more from:
        'Enterprise WiFi','Enterprise Wired','External Mobile','External WiFi','External Wired','Unknown','Unavailable'
         
        .PARAMETER CalleeNetworkTypes
        Show sessions where the selected network type was used by the callee. Can query for multiple network types. Case sensitive. Choose one or more from:
        'Enterprise WiFi','Enterprise Wired','External Mobile','External WiFi','External Wired','Unknown','Unavailable'
 
        .PARAMETER Platforms
        Show sessions where the selected platform was used by either caller or callee. Can query for multiple platforms. Case sensitive. Choose one or more from:
        'SKYPE','CISCO','CISCO_CMS','CISCO_VKM','TEAMS','SKYPE_ONLINE','CISCO_CMS_VKM','AVAYA_AURA_CM','RIG','LYNC_VKM','SKYPE_FOR_BUSINESS_VKM','CISCO_UNITY','CISCO_EXPRESSWAY','AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','ZOOM','ENDPOINT_CLIENT','MISCELLANEOUS'
 
        .PARAMETER CallerPlatforms
        Show sessions where the selected platform was used by the caller. Can query for multiple platforms. Case sensitive. Choose one or more from:
        'SKYPE','CISCO','CISCO_CMS','CISCO_VKM','TEAMS','SKYPE_ONLINE','CISCO_CMS_VKM','AVAYA_AURA_CM','RIG','LYNC_VKM','SKYPE_FOR_BUSINESS_VKM','CISCO_UNITY','CISCO_EXPRESSWAY','AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','ZOOM','ENDPOINT_CLIENT','MISCELLANEOUS'
         
        .PARAMETER CalleePlatforms
        Show sessions where the selected platform was used by the callee. Can query for multiple platforms. Case sensitive. Choose one or more from:
        'SKYPE','CISCO','CISCO_CMS','CISCO_VKM','TEAMS','SKYPE_ONLINE','CISCO_CMS_VKM','AVAYA_AURA_CM','RIG','LYNC_VKM','SKYPE_FOR_BUSINESS_VKM','CISCO_UNITY','CISCO_EXPRESSWAY','AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','ZOOM','ENDPOINT_CLIENT','MISCELLANEOUS'
 
        .PARAMETER Scenarios
        Show sessions where the selected scenario was used by either caller or callee. Can query for multiple scenarios. Choose one or more from:
        'External','Internal','Federated','Unknown'
 
        .PARAMETER CallerScenarios
        Show sessions where the selected scenario was used by the caller. Can query for multiple scenarios. Choose one or more from:
        'External','Internal','Federated','Unknown'
         
        .PARAMETER CalleeScenarios
        Show sessions where the selected scenario was used by the callee. Can query for multiple scenarios. Choose one or more from:
        'External','Internal','Federated','Unknown'
 
        .PARAMETER Subnets
        Show sessions where the selected subnet was used by either caller or callee. Can query for multiple subnets.
 
        .PARAMETER CallerSubnets
        Show sessions where the selected subnet was used by the caller. Can query for multiple subnets.
         
        .PARAMETER CalleeSubnets
        Show sessions where the selected subnet was used by the callee. Can query for multiple subnets.
         
        .PARAMETER Users
        Show sessions where the selected user was either caller or callee. Can query for multiple users.
 
        .PARAMETER FromUsers
        Show sessions where the selected user was the caller. Can query for multiple users.
         
        .PARAMETER ToUsers
        Show sessions where the selected user was the callee. Can query for multiple users.
 
        .PARAMETER ConfOrganizers
        Show sessions hosted by a specified conference organizer. Can query for multiple organizers.
         
        .PARAMETER VPN
        Show sessions where the selected VPN was used by either caller or callee.
 
        .PARAMETER CallerVPN
        Show sessions where the selected VPN was used by the caller.
         
        .PARAMETER CalleeVPN
        Show sessions where the selected VPN was used by the callee.
         
        .PARAMETER ParticipantsMinCount
        Show sessions where the number of participants is greater than or equal to the entered value
 
        .PARAMETER ParticipantsMaxCount
        Show sessions where the number of participants is less than or equal to the entered value
         
        .PARAMETER FeedbackRating
        Show sessions where users provided specific feedback ratings from BAD to EXCELLENT.
        Allowed values are BAD, POOR, FAIR, GOOD, EXCELLENT. Corresponds to ratings from 1 to 5 stars.
         
        .PARAMETER OrderByField
        Sort the output by the selected field
         
        .PARAMETER OrderDirection
        Sort direction. Use with OrderByField. Not case sensitive. Choose from:
        ASC, DESC
 
        .PARAMETER TenantName
        The name of the Nectar 10 tenant. Used in multi-tenant configurations.
         
        .PARAMETER PageSize
        The size of the page used to return data. Defaults to 1000
         
        .PARAMETER ResultSize
        The total number of results to return. Defaults to 1000. Maximum result size is 9,999,999 results
         
        .EXAMPLE
        Get-NectarModalityQualitySummary -Modality AUDIO -TimePeriod LAST_DAY
        Returns the modality quality summary for the past day
         
        .NOTES
        Version 1.1
    #>

    
    [Alias("gnmqs")]
    Param (
        [parameter(Mandatory=$True)]
        [ValidateSet('AUDIO','VIDEO','APPSHARE', IgnoreCase=$True)]
        [string]$Modality,
        [parameter(Mandatory=$False)]
        [ValidateSet('LAST_HOUR','LAST_DAY','LAST_WEEK','LAST_MONTH','CUSTOM', IgnoreCase=$True)]
        [string]$TimePeriod = 'LAST_HOUR',
        [parameter(Mandatory=$False)]
        [Alias("StartDateFrom")]
        [string]$TimePeriodFrom,
        [parameter(Mandatory=$False)]
        [Alias("StartDateTo")]
        [string]$TimePeriodTo,
        [parameter(Mandatory=$False)]
        [ValidateSet('GOOD','POOR_0_25','PARTIALLY_GOOD_25_50','PARTIALLY_GOOD_50_75','PARTIALLY_GOOD_75_100','UNAVAILABLE','UNKNOWN', IgnoreCase=$True)]
        [string[]]$SessionQualities,
        [Parameter(Mandatory=$False)]
        [ValidateRange(0,99999999)]
        [int]$DurationFrom = 0,
        [Parameter(Mandatory=$False)]
        [ValidateRange(0,99999999)]
        [int]$DurationTo = 99999999,
        [parameter(Mandatory=$False)]
        [ValidateSet('TCP','UDP','Unknown', IgnoreCase=$False)]
        [string[]]$Protocols,
        [Parameter(Mandatory=$False)]
        [ValidateRange(200,699)]
        [string[]]$ResponseCodes,
        [parameter(Mandatory=$False)]
        [ValidateSet('INTERNAL','EXTERNAL','FEDERATED','INTERNAL_EXTERNAL','EXTERNAL_INTERNAL','FEDERATED_INTERNAL','INTERNAL_FEDERATED','FEDERATED_EXTERNAL','EXTERNAL_FEDERATED','UNKNOWN', IgnoreCase=$True)]
        [string[]]$SessionScenarios,
        [parameter(Mandatory=$False)]
        [ValidateSet('CONFERENCE','CONFERENCE_SESSION','PEER2PEER','PEER2PEER_MULTIMEDIA','PSTN', IgnoreCase=$False)]
        [string[]]$SessionTypes,
        [parameter(Mandatory=$False)]
        [string[]]$Codecs,
        [parameter(Mandatory=$False)]
        [string[]]$CallerCodecs,
        [parameter(Mandatory=$False)]
        [string[]]$CalleeCodecs,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$Devices,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerDevices,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeDevices,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CaptureDevices,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerCaptureDevices,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeCaptureDevices,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$RenderDevices,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerRenderDevices,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeRenderDevices,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$DeviceVersions,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerDeviceVersions,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeDeviceVersions,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ipaddress[]]$IPAddresses,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ipaddress[]]$CallerIPAddresses,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ipaddress[]]$CalleeIPAddresses,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias("SiteName")]
        [string[]]$Locations,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerLocations,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeLocations,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ExtCities,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerExtCities,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeExtCities,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ExtCountries,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerExtCountries,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeExtCountries,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ExtISPs,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerExtISPs,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeExtISPs,
        [parameter(Mandatory=$False)]
        [ValidateSet('Enterprise WiFi','Enterprise Wired','External Mobile','External WiFi','External Wired','Unknown','Unavailable', IgnoreCase=$False)]
        [string[]]$NetworkTypes,
        [parameter(Mandatory=$False)]
        [ValidateSet('Enterprise WiFi','Enterprise Wired','External Mobile','External WiFi','External Wired','Unknown','Unavailable', IgnoreCase=$False)]
        [string[]]$CallerNetworkTypes,
        [parameter(Mandatory=$False)]
        [ValidateSet('Enterprise WiFi','Enterprise Wired','External Mobile','External WiFi','External Wired','Unknown','Unavailable', IgnoreCase=$False)]
        [string[]]$CalleeNetworkTypes,
        [parameter(Mandatory=$False)]
        [ValidateSet('SKYPE','CISCO','CISCO_CMS','CISCO_VKM','TEAMS','SKYPE_ONLINE','CISCO_CMS_VKM','AVAYA_AURA_CM','RIG','LYNC_VKM','SKYPE_FOR_BUSINESS_VKM','CISCO_UNITY','CISCO_EXPRESSWAY','AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','ZOOM','ENDPOINT_CLIENT','MISCELLANEOUS', IgnoreCase=$True)]
        [string[]]$Platforms,
        [parameter(Mandatory=$False)]
        [ValidateSet('SKYPE','CISCO','CISCO_CMS','CISCO_VKM','TEAMS','SKYPE_ONLINE','CISCO_CMS_VKM','AVAYA_AURA_CM','RIG','LYNC_VKM','SKYPE_FOR_BUSINESS_VKM','CISCO_UNITY','CISCO_EXPRESSWAY','AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','ZOOM','ENDPOINT_CLIENT','MISCELLANEOUS', IgnoreCase=$True)]
        [string[]]$CallerPlatforms,
        [parameter(Mandatory=$False)]
        [ValidateSet('SKYPE','CISCO','CISCO_CMS','CISCO_VKM','TEAMS','SKYPE_ONLINE','CISCO_CMS_VKM','AVAYA_AURA_CM','RIG','LYNC_VKM','SKYPE_FOR_BUSINESS_VKM','CISCO_UNITY','CISCO_EXPRESSWAY','AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','ZOOM','ENDPOINT_CLIENT','MISCELLANEOUS', IgnoreCase=$True)]
        [string[]]$CalleePlatforms,
        [parameter(Mandatory=$False)]
        [ValidateSet('External','Internal','Federated','Unknown', IgnoreCase=$True)]
        [string[]]$Scenarios,
        [parameter(Mandatory=$False)]
        [ValidateSet('External','Internal','Federated','Unknown', IgnoreCase=$True)]
        [string[]]$CallerScenarios,
        [parameter(Mandatory=$False)]
        [ValidateSet('External','Internal','Federated','Unknown', IgnoreCase=$True)]
        [string[]]$CalleeScenarios,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ipaddress[]]$Subnets,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ipaddress[]]$CallerSubnets,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ipaddress[]]$CalleeSubnets,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$Users,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$FromUsers,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ToUsers,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ConfOrganizers,
        [parameter(Mandatory=$False)]
        [ValidateSet('TRUE','FALSE','UNKNOWN', IgnoreCase=$True)]
        [string[]]$VPN,
        [parameter(Mandatory=$False)]
        [ValidateSet('TRUE','FALSE','UNKNOWN', IgnoreCase=$True)]
        [string[]]$CallerVPN,
        [parameter(Mandatory=$False)]
        [ValidateSet('TRUE','FALSE','UNKNOWN', IgnoreCase=$True)]
        [string[]]$CalleeVPN,
        [parameter(Mandatory=$False)]
        [ValidateRange(0,99999)]
        [int]$ParticipantsMinCount,
        [parameter(Mandatory=$False)]
        [ValidateRange(0,99999)]
        [int]$ParticipantsMaxCount,
        [parameter(Mandatory=$False)]
        [ValidateSet('BAD','POOR','FAIR','GOOD','EXCELLENT', IgnoreCase=$False)]
        [string[]]$FeedbackRating,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [parameter(Mandatory=$False)]
        [ValidateSet('DEFAULT','USERS', IgnoreCase=$True)]
        [string]$Scope = 'DEFAULT',
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,100000)]
        [int]$PageSize = 1000,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,9999999)]
        [int]$ResultSize
    )
    
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
        
        $PSBoundParameters.Remove('Modality') | Out-Null
        $FilterSession = Set-NectarFilterParams @PsBoundParameters

        $Modality = $Modality.ToLower()    
        $URI = "https://$Global:NectarCloud/dapi/quality/session/$Modality"
        
        If ($TenantName) { $Params = @{'Tenant' = $TenantName} }

        Try {
            Write-Verbose $PsBoundParameters
            $JSON = Invoke-RestMethod -Method GET -uri $URI -Body $Params -WebSession $FilterSession
            ForEach ($SessionType in $JSON) {
                If ($SessionType.QualityType -in $SessionTypes -or !$SessionTypes) {
                    $QualitySummary = [pscustomobject][ordered]@{}
                    
                    If ($Modality -ne 'appshare') { $QualitySummary | Add-Member -NotePropertyName 'SessionType' -NotePropertyValue $SessionType.QualityType }
                    
                    ForEach ($QualityMetric in $SessionType.QualityMetrics) {
                        $QualitySummary | Add-Member -NotePropertyName $QualityMetric.name -NotePropertyValue $QualityMetric.value
                        $TrendMetricName = $QualityMetric.name + '_TREND'
                        $QualitySummary | Add-Member -NotePropertyName $TrendMetricName -NotePropertyValue $QualityMetric.trends
                    }

                    If ($TenantName) { $QualitySummary | Add-Member -NotePropertyName 'TenantName' -NotePropertyValue $TenantName }    
                    If ($PSItem.SiteName) { $QualitySummary | Add-Member -NotePropertyName 'SiteName' -NotePropertyValue $PSItem.SiteName }
                
                    $QualitySummary | Add-Member -TypeName 'Nectar.ModalityQuality'
                    $QualitySummary
                }
            }
        }
        Catch {
            Write-Error "No results. Try specifying a less-restrictive filter"
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



Function Get-NectarLocationQualitySummary {
    <#
        .SYNOPSIS
        Returns summary information for different locations over a given timeperiod.
 
        .DESCRIPTION
        Returns summary information for different locations over a given timeperiod. This is used to build the SUMMARY page in Nectar 10.
        UI_ELEMENT
                 
        .PARAMETER TimePeriod
        The time period to show session data from. Select from 'LAST_HOUR','LAST_DAY','LAST_WEEK','LAST_MONTH','CUSTOM'.
        CUSTOM requires using TimePeriodFrom and TimePeriodTo parameters.
         
        .PARAMETER TimePeriodFrom
        The earliest date/time to show session data from. Must be used in conjunction with -TimePeriod CUSTOM and TimePeriodTo parameters. Use format 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'. All time/dates in UTC.
         
        .PARAMETER TimePeriodTo
        The latest date/time to show session data from. Must be used in conjunction with -TimePeriod CUSTOM and TimePeriodFrom parameters. Use format 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'. All time/dates in UTC.
         
        .PARAMETER SessionQualities
        Show sessions that match a given quality rating. Case sensitive. Choose one or more from:
        'GOOD','POOR_0_25','PARTIALLY_GOOD_25_50','PARTIALLY_GOOD_50_75','PARTIALLY_GOOD_75_100','UNAVAILABLE','UNKNOWN'
         
        .PARAMETER DurationFrom
        The shortest call length (in seconds) to show session data.
         
        .PARAMETER DurationTo
        The longest call length (in seconds) to show session data.
         
        .PARAMETER Modalities
        Show sessions that match one or more modality types. Not case sensitive. Choose one or more from:
        'AUDIO','VIDEO','APP_SHARING','FILE_TRANSFER','IM','UNKNOWN','TOTAL','VBSS','REMOTE_ASSISTANCE','APP_INVITE','FOCUS','UNKNOWN'
         
        .PARAMETER Protocols
        Show sessions that match one or more network protocol types. Case sensitive. Choose one or more from:
        'TCP','UDP','Unknown'
         
        .PARAMETER ResponseCodes
        Show sessions that match one or more SIP response codes. Accepts numbers from 200 to 699
         
        .PARAMETER SessionScenarios
        Show sessions that match one or more session scenarios. Not case sensitive. Choose one or more from:
        'External','Internal','Internal-External','External-Internal','Federated','Internal-Federated','External-Federated','Unknown'
         
        .PARAMETER SessionTypes
        Show sessions that match one or more session scenarios. Case sensitive. Choose one or more from:
        'Conference','Peer To Peer','Peer To Peer (Multimedia)','PSTN/External'
         
        .PARAMETER Codecs
        Show sessions where the selected codec was used by either caller or callee. Can query for multiple codecs. Case sensitive. Use Get-NectarCodecs for a list of valid codecs.
 
        .PARAMETER CallerCodecs
        Show sessions where the selected codec was used by the caller. Can query for multiple codecs. Case sensitive. Use Get-NectarCodecs for a list of valid codecs.
         
        .PARAMETER CalleeCodecs
        Show sessions where the selected codec was used by the callee. Can query for multiple codecs. Case sensitive. Use Get-NectarCodecs for a list of valid codecs.
         
        .PARAMETER Devices
        Show sessions where the selected device was used by either caller or callee. Can query for multiple devices. Case sensitive. Use Get-NectarSupportedDevice for a list of valid devices.
 
        .PARAMETER CallerDevices
        Show sessions where the selected device was used by the caller. Can query for multiple devices. Case sensitive. Use Get-NectarSupportedDevice for a list of valid devices.
         
        .PARAMETER CalleeDevices
        Show sessions where the selected device was used by the callee. Can query for multiple devices. Case sensitive. Use Get-NectarSupportedDevice for a list of valid devices.
 
        .PARAMETER CaptureDevices
        Show sessions where the selected capture device (microphone) was used by either caller or callee. Can query for multiple devices. Case sensitive. Use Get-NectarSupportedDevice for a list of valid devices.
 
        .PARAMETER CallerCaptureDevices
        Show sessions where the selected capture device (microphone) was used by the caller. Can query for multiple devices. Case sensitive. Use Get-NectarSupportedDevice for a list of valid devices.
         
        .PARAMETER CalleeCaptureDevices
        Show sessions where the selected capture device (microphone) was used by the callee. Can query for multiple devices. Case sensitive. Use Get-NectarSupportedDevice for a list of valid devices.
 
        .PARAMETER RenderDevices
        Show sessions where the selected render device (speaker) was used by either caller or callee. Can query for multiple devices. Case sensitive. Use Get-NectarSupportedDevice for a list of valid devices.
 
        .PARAMETER CallerRenderDevices
        Show sessions where the selected render device (speaker) was used by the caller. Can query for multiple devices. Case sensitive. Use Get-NectarSupportedDevice for a list of valid devices.
         
        .PARAMETER CalleeRenderDevices
        Show sessions where the selected render device (speaker) was used by the callee. Can query for multiple devices. Case sensitive. Use Get-NectarSupportedDevice for a list of valid devices.
 
        .PARAMETER DeviceVersions
        Show sessions where the selected device version was used by either caller or callee. Can query for multiple devices. Case sensitive. Use Get-NectarClientVersion for a list of valid client versions.
 
        .PARAMETER CallerDeviceVersions
        Show sessions where the selected device version was used by the caller. Can query for multiple devices. Case sensitive. Use Get-NectarClientVersion for a list of valid client versions.
         
        .PARAMETER CalleeDeviceVersions
        Show sessions where the selected device version was used by the callee. Can query for multiple devices. Case sensitive. Use Get-NectarClientVersion for a list of valid client versions.
 
        .PARAMETER IPAddresses
        Show sessions where the selected IP address was used by either caller or callee. Can query for multiple IPs.
 
        .PARAMETER CallerIPAddresses
        Show sessions where the selected IP address was used by the caller. Can query for multiple IPs.
         
        .PARAMETER CalleeIPAddresses
        Show sessions where the selected IP address was used by the callee. Can query for multiple IPs.
         
        .PARAMETER Locations
        Show sessions where the selected location was used by either caller or callee. Can query for multiple locations.
 
        .PARAMETER CallerLocations
        Show sessions where the selected location was used by the caller. Can query for multiple locations.
         
        .PARAMETER CalleeLocations
        Show sessions where the selected location was used by the callee. Can query for multiple locations.
                 
        .PARAMETER ExtCities
        Show sessions where the caller or callee was located in the selected city (as detected via geolocating the user's external IP address). Can query for multiple cities.
 
        .PARAMETER CallerExtCities
        Show sessions where the caller was located in the selected city (as detected via geolocating the user's external IP address). Can query for multiple cities.
         
        .PARAMETER CalleeExtCities
        Show sessions where the callee was located in the selected city (as detected via geolocating the user's external IP address). Can query for multiple cities.
         
        .PARAMETER ExtCountries
        Show sessions where the caller or callee was located in the selected country (as detected via geolocating the user's external IP address). Can query for multiple countries.
 
        .PARAMETER CallerExtCountries
        Show sessions where the caller was located in the selected country (as detected via geolocating the user's external IP address). Can query for multiple countries.
         
        .PARAMETER CalleeExtCountries
        Show sessions where the callee was located in the selected country (as detected via geolocating the user's external IP address). Can query for multiple countries.
         
        .PARAMETER ExtISPs
        Show sessions where the caller or callee was located in the selected ISP (as detected via geolocating the user's external IP address). Can query for multiple ISPs.
 
        .PARAMETER CallerExtISPs
        Show sessions where the caller was located in the selected ISP (as detected via geolocating the user's external IP address). Can query for multiple ISPs.
         
        .PARAMETER CalleeExtISPs
        Show sessions where the callee was located in the selected ISP (as detected via geolocating the user's external IP address). Can query for multiple ISPs.
                 
        .PARAMETER NetworkTypes
        Show sessions where the selected network type was used by either caller or callee. Can query for multiple network types. Case sensitive. Choose one or more from:
        'Enterprise WiFi','Enterprise Wired','External Mobile','External WiFi','External Wired','Unknown','Unavailable'
 
        .PARAMETER CallerNetworkTypes
        Show sessions where the selected network type was used by the caller. Can query for multiple network types. Case sensitive. Choose one or more from:
        'Enterprise WiFi','Enterprise Wired','External Mobile','External WiFi','External Wired','Unknown','Unavailable'
         
        .PARAMETER CalleeNetworkTypes
        Show sessions where the selected network type was used by the callee. Can query for multiple network types. Case sensitive. Choose one or more from:
        'Enterprise WiFi','Enterprise Wired','External Mobile','External WiFi','External Wired','Unknown','Unavailable'
 
        .PARAMETER Platforms
        Show sessions where the selected platform was used by either caller or callee. Can query for multiple platforms. Case sensitive. Choose one or more from:
        'SKYPE','CISCO','CISCO_CMS','CISCO_VKM','TEAMS','SKYPE_ONLINE','CISCO_CMS_VKM','AVAYA_AURA_CM','RIG','LYNC_VKM','SKYPE_FOR_BUSINESS_VKM','CISCO_UNITY','CISCO_EXPRESSWAY','AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','ZOOM','ENDPOINT_CLIENT','MISCELLANEOUS'
 
        .PARAMETER CallerPlatforms
        Show sessions where the selected platform was used by the caller. Can query for multiple platforms. Case sensitive. Choose one or more from:
        'SKYPE','CISCO','CISCO_CMS','CISCO_VKM','TEAMS','SKYPE_ONLINE','CISCO_CMS_VKM','AVAYA_AURA_CM','RIG','LYNC_VKM','SKYPE_FOR_BUSINESS_VKM','CISCO_UNITY','CISCO_EXPRESSWAY','AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','ZOOM','ENDPOINT_CLIENT','MISCELLANEOUS'
         
        .PARAMETER CalleePlatforms
        Show sessions where the selected platform was used by the callee. Can query for multiple platforms. Case sensitive. Choose one or more from:
        'SKYPE','CISCO','CISCO_CMS','CISCO_VKM','TEAMS','SKYPE_ONLINE','CISCO_CMS_VKM','AVAYA_AURA_CM','RIG','LYNC_VKM','SKYPE_FOR_BUSINESS_VKM','CISCO_UNITY','CISCO_EXPRESSWAY','AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','ZOOM','ENDPOINT_CLIENT','MISCELLANEOUS'
 
        .PARAMETER Scenarios
        Show sessions where the selected scenario was used by either caller or callee. Can query for multiple scenarios. Choose one or more from:
        'External','Internal','Federated','Unknown'
 
        .PARAMETER CallerScenarios
        Show sessions where the selected scenario was used by the caller. Can query for multiple scenarios. Choose one or more from:
        'External','Internal','Federated','Unknown'
         
        .PARAMETER CalleeScenarios
        Show sessions where the selected scenario was used by the callee. Can query for multiple scenarios. Choose one or more from:
        'External','Internal','Federated','Unknown'
 
        .PARAMETER Subnets
        Show sessions where the selected subnet was used by either caller or callee. Can query for multiple subnets.
 
        .PARAMETER CallerSubnets
        Show sessions where the selected subnet was used by the caller. Can query for multiple subnets.
         
        .PARAMETER CalleeSubnets
        Show sessions where the selected subnet was used by the callee. Can query for multiple subnets.
         
        .PARAMETER Users
        Show sessions where the selected user was either caller or callee. Can query for multiple users.
 
        .PARAMETER FromUsers
        Show sessions where the selected user was the caller. Can query for multiple users.
         
        .PARAMETER ToUsers
        Show sessions where the selected user was the callee. Can query for multiple users.
 
        .PARAMETER ConfOrganizers
        Show sessions hosted by a specified conference organizer. Can query for multiple organizers.
         
        .PARAMETER VPN
        Show sessions where the selected VPN was used by either caller or callee.
 
        .PARAMETER CallerVPN
        Show sessions where the selected VPN was used by the caller.
         
        .PARAMETER CalleeVPN
        Show sessions where the selected VPN was used by the callee.
         
        .PARAMETER ParticipantsMinCount
        Show sessions where the number of participants is greater than or equal to the entered value
 
        .PARAMETER ParticipantsMaxCount
        Show sessions where the number of participants is less than or equal to the entered value
         
        .PARAMETER FeedbackRating
        Show sessions where users provided specific feedback ratings from BAD to EXCELLENT.
        Allowed values are BAD, POOR, FAIR, GOOD, EXCELLENT. Corresponds to ratings from 1 to 5 stars.
         
        .PARAMETER OrderByField
        Sort the output by the selected field
         
        .PARAMETER OrderDirection
        Sort direction. Use with OrderByField. Not case sensitive. Choose from:
        ASC, DESC
 
        .PARAMETER TenantName
        The name of the Nectar 10 tenant. Used in multi-tenant configurations.
         
        .PARAMETER PageSize
        The size of the page used to return data. Defaults to 1000
         
        .PARAMETER ResultSize
        The total number of results to return. Defaults to 1000. Maximum result size is 9,999,999 results
         
        .EXAMPLE
        Get-NectarLocationQualitySummary -TimePeriod LAST_DAY
        Returns the quality summary for each location for the last day
         
        .NOTES
        Version 1.1
    #>

    
    [Alias("gnlqs")]
    Param (
        [parameter(Mandatory=$False)]
        [ValidateSet('LAST_HOUR','LAST_DAY','LAST_WEEK','LAST_MONTH','CUSTOM', IgnoreCase=$True)]
        [string]$TimePeriod = 'LAST_HOUR',
        [parameter(Mandatory=$False)]
        [Alias("StartDateFrom")]
        [string]$TimePeriodFrom,
        [parameter(Mandatory=$False)]
        [Alias("StartDateTo")]
        [string]$TimePeriodTo,
        [parameter(Mandatory=$False)]
        [ValidateSet('GOOD','POOR_0_25','PARTIALLY_GOOD_25_50','PARTIALLY_GOOD_50_75','PARTIALLY_GOOD_75_100','UNAVAILABLE','UNKNOWN', IgnoreCase=$True)]
        [string[]]$SessionQualities,
        [Parameter(Mandatory=$False)]
        [ValidateRange(0,99999999)]
        [int]$DurationFrom = 0,
        [Parameter(Mandatory=$False)]
        [ValidateRange(0,99999999)]
        [int]$DurationTo = 99999999,
        [parameter(Mandatory=$False)]
        [ValidateSet('AUDIO','VIDEO','APP_SHARING','FILE_TRANSFER','IM','UNKNOWN','TOTAL','VBSS','REMOTE_ASSISTANCE','APP_INVITE','FOCUS','UNKNOWN', IgnoreCase=$True)]
        [string[]]$Modalities,
        [parameter(Mandatory=$False)]
        [ValidateSet('TCP','UDP','Unknown', IgnoreCase=$False)]
        [string[]]$Protocols,
        [Parameter(Mandatory=$False)]
        [ValidateRange(200,699)]
        [string[]]$ResponseCodes,
        [parameter(Mandatory=$False)]
        [ValidateSet('INTERNAL','EXTERNAL','FEDERATED','INTERNAL_EXTERNAL','EXTERNAL_INTERNAL','FEDERATED_INTERNAL','INTERNAL_FEDERATED','FEDERATED_EXTERNAL','EXTERNAL_FEDERATED','UNKNOWN', IgnoreCase=$True)]
        [string[]]$SessionScenarios,
        [parameter(Mandatory=$False)]
        [ValidateSet('CONFERENCE','CONFERENCE_SESSION','PEER2PEER','PEER2PEER_MULTIMEDIA','PSTN', IgnoreCase=$False)]
        [string[]]$SessionTypes,
        [parameter(Mandatory=$False)]
        [string[]]$Codecs,
        [parameter(Mandatory=$False)]
        [string[]]$CallerCodecs,
        [parameter(Mandatory=$False)]
        [string[]]$CalleeCodecs,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$Devices,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerDevices,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeDevices,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CaptureDevices,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerCaptureDevices,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeCaptureDevices,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$RenderDevices,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerRenderDevices,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeRenderDevices,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$DeviceVersions,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerDeviceVersions,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeDeviceVersions,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ipaddress[]]$IPAddresses,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ipaddress[]]$CallerIPAddresses,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ipaddress[]]$CalleeIPAddresses,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$Locations,
        [parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerLocations,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeLocations,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ExtCities,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerExtCities,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeExtCities,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ExtCountries,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerExtCountries,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeExtCountries,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ExtISPs,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerExtISPs,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeExtISPs,
        [parameter(Mandatory=$False)]
        [ValidateSet('Enterprise WiFi','Enterprise Wired','External Mobile','External WiFi','External Wired','Unknown','Unavailable', IgnoreCase=$False)]
        [string[]]$NetworkTypes,
        [parameter(Mandatory=$False)]
        [ValidateSet('Enterprise WiFi','Enterprise Wired','External Mobile','External WiFi','External Wired','Unknown','Unavailable', IgnoreCase=$False)]
        [string[]]$CallerNetworkTypes,
        [parameter(Mandatory=$False)]
        [ValidateSet('Enterprise WiFi','Enterprise Wired','External Mobile','External WiFi','External Wired','Unknown','Unavailable', IgnoreCase=$False)]
        [string[]]$CalleeNetworkTypes,
        [parameter(Mandatory=$False)]
        [ValidateSet('SKYPE','CISCO','CISCO_CMS','CISCO_VKM','TEAMS','SKYPE_ONLINE','CISCO_CMS_VKM','AVAYA_AURA_CM','RIG','LYNC_VKM','SKYPE_FOR_BUSINESS_VKM','CISCO_UNITY','CISCO_EXPRESSWAY','AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','ZOOM','ENDPOINT_CLIENT','MISCELLANEOUS', IgnoreCase=$True)]
        [string[]]$Platforms,
        [parameter(Mandatory=$False)]
        [ValidateSet('SKYPE','CISCO','CISCO_CMS','CISCO_VKM','TEAMS','SKYPE_ONLINE','CISCO_CMS_VKM','AVAYA_AURA_CM','RIG','LYNC_VKM','SKYPE_FOR_BUSINESS_VKM','CISCO_UNITY','CISCO_EXPRESSWAY','AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','ZOOM','ENDPOINT_CLIENT','MISCELLANEOUS', IgnoreCase=$True)]
        [string[]]$CallerPlatforms,
        [parameter(Mandatory=$False)]
        [ValidateSet('SKYPE','CISCO','CISCO_CMS','CISCO_VKM','TEAMS','SKYPE_ONLINE','CISCO_CMS_VKM','AVAYA_AURA_CM','RIG','LYNC_VKM','SKYPE_FOR_BUSINESS_VKM','CISCO_UNITY','CISCO_EXPRESSWAY','AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','ZOOM','ENDPOINT_CLIENT','MISCELLANEOUS', IgnoreCase=$True)]
        [string[]]$CalleePlatforms,
        [parameter(Mandatory=$False)]
        [ValidateSet('External','Internal','Federated','Unknown', IgnoreCase=$True)]
        [string[]]$Scenarios,
        [parameter(Mandatory=$False)]
        [ValidateSet('External','Internal','Federated','Unknown', IgnoreCase=$True)]
        [string[]]$CallerScenarios,
        [parameter(Mandatory=$False)]
        [ValidateSet('External','Internal','Federated','Unknown', IgnoreCase=$True)]
        [string[]]$CalleeScenarios,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ipaddress[]]$Subnets,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ipaddress[]]$CallerSubnets,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ipaddress[]]$CalleeSubnets,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$Users,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$FromUsers,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ToUsers,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ConfOrganizers,
        [parameter(Mandatory=$False)]
        [ValidateSet('TRUE','FALSE','UNKNOWN', IgnoreCase=$True)]
        [string[]]$VPN,
        [parameter(Mandatory=$False)]
        [ValidateSet('TRUE','FALSE','UNKNOWN', IgnoreCase=$True)]
        [string[]]$CallerVPN,
        [parameter(Mandatory=$False)]
        [ValidateSet('TRUE','FALSE','UNKNOWN', IgnoreCase=$True)]
        [string[]]$CalleeVPN,
        [parameter(Mandatory=$False)]
        [ValidateRange(0,99999)]
        [int]$ParticipantsMinCount,
        [parameter(Mandatory=$False)]
        [ValidateRange(0,99999)]
        [int]$ParticipantsMaxCount,
        [parameter(Mandatory=$False)]
        [ValidateSet('BAD','POOR','FAIR','GOOD','EXCELLENT', IgnoreCase=$False)]
        [string[]]$FeedbackRating,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [parameter(Mandatory=$False)]
        [ValidateSet('DEFAULT','USERS', IgnoreCase=$True)]
        [string]$Scope = 'DEFAULT',
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,100000)]
        [int]$PageSize = 1000,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,9999999)]
        [int]$ResultSize
    )
    
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
        
        $FilterSession = Set-NectarFilterParams @PsBoundParameters

        If ($TenantName) { $Params = @{'Tenant' = $TenantName} }
        
        Try {
            $JSON = Invoke-RestMethod -Method GET -uri "https://$Global:NectarCloud/dapi/quality/locations" -Body $Params -WebSession $FilterSession
            $JSONGoodPoor = Invoke-RestMethod -Method GET -uri "https://$Global:NectarCloud/dapi/quality/map" -Body $Params -WebSession $FilterSession

            ForEach ($Location in $JSON) {
                $LocGoodPoor = $JSONGoodPoor | Where {$_.name -eq $Location.Name}
                $QualitySummary = [pscustomobject][ordered]@{
                    'LocationName' = $Location.Name
                    'Sessions' = $Location.Data.Sessions
                    'CurrentSessions' = $Location.Data.currentSession
                    'GoodSessions' = $LocGoodPoor.Sessions.Good
                    'PoorSessions' = $LocGoodPoor.Sessions.Poor
                    'UnknownSessions' = $LocGoodPoor.Sessions.Unknown
                    'AvgMOS' = $Location.Data.avgMos
                    'MOSTrend' = $Location.Data.mos
                }
                
                If ($TenantName) { $QualitySummary | Add-Member -NotePropertyName 'TenantName' -NotePropertyValue $TenantName }
                
                $QualitySummary | Add-Member -TypeName 'Nectar.LocationQuality'
                
                If ($Location.Name -ne 'All') { $QualitySummary }
            }
        }
        Catch {
            Write-Error "No results. Try specifying a less-restrictive filter"
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}


#################################################################################################################################################
# #
# Event Functions #
# #
#################################################################################################################################################

Function Get-NectarEvent {
    <#
        .SYNOPSIS
        Return a list of current or historic Nectar monitored device events
         
        .DESCRIPTION
        Return a list of current or historic Nectar monitored device events
         
        .PARAMETER TimePeriod
        The time period to show event data from. Select from 'LAST_HOUR','LAST_DAY','LAST_WEEK','LAST_MONTH','CUSTOM'.
        CUSTOM requires using TimePeriodFrom and TimePeriodTo parameters.
 
        .PARAMETER TimePeriodFrom
        The earliest date/time to show event data from. Must be used in conjunction with -TimePeriod CUSTOM and TimePeriodTo parameters. Use format 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'. All time/dates in UTC.
         
        .PARAMETER TimePeriodTo
        The latest date/time to show event data from. Must be used in conjunction with -TimePeriod CUSTOM and TimePeriodFrom parameters. Use format 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'. All time/dates in UTC.
         
        .PARAMETER LastTimeAfter
        Only return results that occurred more recently than the entered value. Use date-time format as in 2020-04-20T17:46:37.554
         
        .PARAMETER EventAlertLevels
        Return only events that meet the specified alert level. Choose one or more from CRITICAL, MAJOR, MINOR, WARNING, GOOD, NO_ACTIVITY
 
        .PARAMETER Locations
        Show alerts for one or more specified locations
 
        .PARAMETER SearchQuery
        Search for events that contain the specified string
         
        .PARAMETER OrderByField
        Order the resultset by the specified field. Choose from id, type, lastTime, displayName, deviceName, description, eventId, time, delay, source, location, sourceId
         
        .PARAMETER EventState
        Return either current events or previously acknowledged events
         
        .PARAMETER TenantName
        The name of the Nectar 10 tenant. Used in multi-tenant configurations.
 
        .PARAMETER PageSize
        The size of the page used to return data. Defaults to 1000
         
        .PARAMETER ResultSize
        The total number of results to return. Defaults to 1000. Maximum result size is 9,999,999 results
 
        .EXAMPLE
        Get-NectarEvent -EventAlertLevels CRITICAL,MAJOR
        Returns a list of current events in the last hour that are either critical or major
         
        .EXAMPLE
        Get-NectarEvent -SearchQuery BadServer -EventState Historic -TimePeriod LAST_WEEK
        Returns a list of historical events from the last week that include the word 'badserver'
 
        .NOTES
        Version 1.0
    #>

    
    Param (
        [parameter(Mandatory=$False)]
        [ValidateSet('LAST_HOUR','LAST_DAY','LAST_WEEK','LAST_MONTH','CUSTOM', IgnoreCase=$True)]
        [string]$TimePeriod = 'LAST_HOUR',
        [parameter(Mandatory=$False)]
        [Alias("StartDateFrom")]
        [string]$TimePeriodFrom,
        [parameter(Mandatory=$False)]
        [Alias("StartDateTo")]
        [string]$TimePeriodTo,
        [parameter(Mandatory=$False)]
        [string]$LastTimeAfter,        
        [parameter(Mandatory=$False)]
        [ValidateSet('CRITICAL', 'MAJOR', 'MINOR', 'WARNING', 'GOOD', 'NO_ACTIVITY', IgnoreCase=$True)]
        [string[]]$EventAlertLevels,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias("SiteName")]
        [string[]]$Locations,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$SearchQuery,
        [parameter(Mandatory=$False)]
        [ValidateSet('id', 'type', 'lastTime', 'displayName', 'deviceName', 'description', 'eventId', 'time', 'delay', 'source', 'location', 'sourceId', IgnoreCase=$True)]
        [string]$OrderByField,
        [parameter(Mandatory=$False)]
        [ValidateSet('asc', 'desc', IgnoreCase=$True)]
        [string]$OrderDirection,
        [parameter(Mandatory=$False)]
        [ValidateSet('Current', 'Historic', IgnoreCase=$True)]
        [string]$EventState = 'Current',        
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,100000)]
        [int]$PageSize = 1000,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,9999999)]
        [int]$ResultSize
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { 
                $TenantName = $Global:NectarTenantName 
            }

            $Params = @{
                'TimePeriod' = $TimePeriod
            }
        
            # Convert any PowerShell array objects to comma-separated strings to add to the GET querystring
            If ($LastTimeAfter) { $Params.Add('LastTimeAfter',$LastTimeAfter) } 
            If ($EventAlertLevels) { $EventAlertLevels | %{$EventAlertLevelsStr += ($(if($EventAlertLevelsStr){","}) + $_)}; $Params.Add('EventAlertLevels',$EventAlertLevelsStr) }
            If ($Locations) { $Locations | %{$LocationsStr += ($(if($LocationsStr){","}) + $_)}; $Params.Add('Locations',$LocationsStr) }
            If ($SearchQuery) { $Params.Add('q',$SearchQuery) }
            If ($OrderByField) { $Params.Add('OrderByField',$OrderByField) }
            If ($OrderDirection) { $Params.Add('OrderDirection',$OrderDirection) }
            If ($TenantName) { $Params.Add('Tenant',$TenantName) }

            # Convert date to UNIX timestamp
            If($TimePeriodFrom) {
                $TimePeriodFrom = (Get-Date -Date $TimePeriodFrom -UFormat %s) + '000'
                $Params.Add('StartDateFrom',$TimePeriodFrom)
            }
            
            If($TimePeriodTo) {
                $TimePeriodTo = (Get-Date -Date $TimePeriodTo -UFormat %s) + '000'
                $Params.Add('StartDateTo',$TimePeriodTo)
            }

            # Set the page size to the result size if -ResultSize switch is used to limit the number of returned items
            # Otherwise, set page size (defaults to 1000)
            If ($ResultSize) {
                $Params.Add('pageSize',$ResultSize)
            }
            Else {
                $Params.Add('pageSize',$PageSize)
            }

            If ($EventState -eq 'Current') {
                $URI = "https://$Global:NectarCloud/dapi/event/current"
            }
            Else {
                $URI = "https://$Global:NectarCloud/dapi/event/historic"
            }
            
            Write-Verbose $URI        
            
            $JSON = Invoke-RestMethod -Method GET -WebSession $Global:NectarSession -uri $URI -Body $Params
            
            $TotalPages = $JSON.totalPages
            
            If ($TenantName) {$JSON.elements | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty}
            $JSON.elements | Add-Member -TypeName 'Nectar.EventList'
            $JSON.elements
            
            If ($TotalPages -gt 1 -and !($ResultSize)) {
                $PageNum = 2
                Write-Verbose "Page size: $PageSize"
                While ($PageNum -le $TotalPages) {
                    Write-Verbose "Working on page $PageNum of $TotalPages"
                    $PagedURI = $URI + "?pageNumber=$PageNum"
                    $JSON = Invoke-RestMethod -Method GET -uri $PagedURI -Body $Params -WebSession $Global:NectarSession
                    If ($TenantName) {$JSON.elements | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty}
                    $JSON.elements | Add-Member -TypeName 'Nectar.SessionList'
                    $JSON.elements
                    $PageNum++
                }
            }
        }
        Catch {
            Write-Error "No results. Try specifying a less-restrictive filter"
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



Function Get-NectarEventDetail {
    <#
        .SYNOPSIS
        Return information about a specific event
         
        .DESCRIPTION
        Return information about a specific event
 
        .PARAMETER EventID
        The ID of the event to return details about
         
        .PARAMETER EventState
        Return either current events or previously acknowledged events
         
        .PARAMETER TenantName
        The name of the Nectar 10 tenant. Used in multi-tenant configurations.
 
        .EXAMPLE
        Get-MSTeamsCallRecord -CallRecordID ed672235-5417-40ce-8425-12b8b702a505
 
        .NOTES
        Version 1.0
    #>

    
    Param (
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [string]$ID,
        [parameter(Mandatory=$False)]
        [ValidateSet('Current', 'Historic', IgnoreCase=$True)]
        [string]$EventState = 'Current',        
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { 
                $TenantName = $Global:NectarTenantName 
            }

            $Params = @{
                'eventId' = $ID
            }
        
            If ($TenantName) { $Params.Add('Tenant',$TenantName) }

            If ($EventState -eq 'Current') {
                $URI = "https://$Global:NectarCloud/dapi/event/current/view"
            }
            Else {
                $URI = "https://$Global:NectarCloud/dapi/event/historic/view"
            }
            
            Write-Verbose $URI        
            
            $JSON = Invoke-RestMethod -Method GET -Credential $Global:NectarCred -uri $URI -Body $Params    
            
            If ($TenantName) {$JSON | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty}
            
            $JSON | Add-Member -TypeName 'Nectar.EventDetail'
            $JSON
        }
        Catch {
            Write-Error "Could not find event with ID $EventID"
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}





#################################################################################################################################################
# #
# Platform Functions #
# #
#################################################################################################################################################

Function Get-NectarPlatformItems {
    <#
        .SYNOPSIS
        Return information about all the platforms installed
         
        .DESCRIPTION
        Return information about all the platforms installed
 
        .PARAMETER Platform
        Show information about selected platform. Choose one or more from: 'AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','CISCO','CISCO_CMS','CISCO_VKM','SKYPE','SKYPE_ONLINE','TEAMS'
         
        .PARAMETER TimePeriod
        The time period to show event data from. Select from 'LAST_HOUR','LAST_DAY','LAST_WEEK','LAST_MONTH','CUSTOM'.
        CUSTOM requires using TimePeriodFrom and TimePeriodTo parameters.
 
        .PARAMETER TimePeriodFrom
        The earliest date/time to show event data from. Must be used in conjunction with -TimePeriod CUSTOM and TimePeriodTo parameters. Use format 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'. All time/dates in UTC. Use date-time format as in 2020-04-20T17:46:37.554
         
        .PARAMETER TimePeriodTo
        The latest date/time to show event data from. Must be used in conjunction with -TimePeriod CUSTOM and TimePeriodFrom parameters. Use format 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'. All time/dates in UTC. Use date-time format as in 2020-04-20T17:46:37.554
         
        .PARAMETER TenantName
        The name of the Nectar 10 tenant. Used in multi-tenant configurations.
         
        .PARAMETER PageSize
        The size of the page used to return data. Defaults to 1000
         
        .PARAMETER ResultSize
        The total number of results to return. Defaults to 1000. Maximum result size is 9,999,999 results
 
        .EXAMPLE
        Get-NectarPlatformItems -Platform CISCO
 
        .NOTES
        Version 1.0
    #>

    
    Param (
        [parameter(Mandatory=$True)]
        [ValidateSet('AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','SKYPE','CISCO','CISCO_CMS','CISCO_VKM','TEAMS','SKYPE_ONLINE','CISCO_CMS_VKM','AVAYA_AURA_CM','RIG','LYNC_VKM','SKYPE_FOR_BUSINESS_VKM','CISCO_UNITY','CISCO_EXPRESSWAY','AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','ZOOM','ENDPOINT_CLIENT','MISCELLANEOUS', IgnoreCase=$True)]
        [string]$Platform,
        [parameter(Mandatory=$False)]
        [ValidateSet('LAST_HOUR','LAST_DAY','LAST_WEEK','LAST_MONTH','CUSTOM', IgnoreCase=$True)]
        [string]$TimePeriod = 'LAST_HOUR',
        [parameter(Mandatory=$False)]
        [Alias("StartDateFrom")]
        [string]$TimePeriodFrom,
        [parameter(Mandatory=$False)]
        [Alias("StartDateTo")]
        [string]$TimePeriodTo,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,100000)]
        [int]$PageSize = 1000,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,9999999)]
        [int]$ResultSize
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        $Params = @{
            'TimePeriod' = $TimePeriod
            'Platform' = $Platform
        }

        # Convert date to UNIX timestamp
        If($TimePeriodFrom) {
            $TimePeriodFrom = (Get-Date -Date $TimePeriodFrom -UFormat %s) + '000'
            $Params.Add('StartDateFrom',$TimePeriodFrom)
        }
        
        If($TimePeriodTo) {
            $TimePeriodTo = (Get-Date -Date $TimePeriodTo -UFormat %s) + '000'
            $Params.Add('StartDateTo',$TimePeriodTo)
        }
        
        Try {
        
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { 
                $TenantName = $Global:NectarTenantName 
            }
    
            If ($TenantName) { $Params.Add('Tenant',$TenantName) }

            $URI = "https://$Global:NectarCloud/dapi/platform/clusters"
            Write-Verbose $URI        
            
            $JSON = Invoke-RestMethod -Method GET -Credential $Global:NectarCred -uri $URI -Body $Params    
            
            If ($TenantName) {$JSON | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty}
            
            $JSON.elements
        }
        Catch {
            Write-Error "Could not get platform items"
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



Function Get-NectarPlatformItemSummary {
    <#
        .SYNOPSIS
        Return summary information about a specific platform item
         
        .DESCRIPTION
        Return summary information about a specific platform item
         
        .PARAMETER ClusterID
        The ID of a cluster to return summary information
         
        .PARAMETER Platform
        Show information about selected platform. Choose one or more from: 'AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','CISCO','CISCO_CMS','CISCO_VKM','SKYPE','SKYPE_ONLINE','TEAMS'
 
        .PARAMETER Source
        Show information about either events, current status or both
         
        .PARAMETER TimePeriod
        The time period to show event data from. Select from 'LAST_HOUR','LAST_DAY','LAST_WEEK','LAST_MONTH','CUSTOM'.
        CUSTOM requires using TimePeriodFrom and TimePeriodTo parameters.
 
        .PARAMETER TimePeriodFrom
        The earliest date/time to show event data from. Must be used in conjunction with -TimePeriod CUSTOM and TimePeriodTo parameters. Use format 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'. All time/dates in UTC. Use date-time format as in 2020-04-20T17:46:37.554
         
        .PARAMETER TimePeriodTo
        The latest date/time to show event data from. Must be used in conjunction with -TimePeriod CUSTOM and TimePeriodFrom parameters. Use format 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'. All time/dates in UTC. Use date-time format as in 2020-04-20T17:46:37.554
         
        .PARAMETER TenantName
        The name of the Nectar 10 tenant. Used in multi-tenant configurations.
         
        .PARAMETER PageSize
        The size of the page used to return data. Defaults to 1000
         
        .PARAMETER ResultSize
        The total number of results to return. Defaults to 1000. Maximum result size is 9,999,999 results
 
        .EXAMPLE
        Get-NectarPlatformItemSummary -Platform CISCO -ClusterID 3_1
 
        .NOTES
        Version 1.0
    #>

    
    Param (
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [string]$ClusterID,
        [parameter(Mandatory=$True)]
        [ValidateSet('AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','SKYPE','CISCO','CISCO_CMS','CISCO_VKM','TEAMS','SKYPE_ONLINE','CISCO_CMS_VKM','AVAYA_AURA_CM','RIG','LYNC_VKM','SKYPE_FOR_BUSINESS_VKM','CISCO_UNITY','CISCO_EXPRESSWAY','AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','ZOOM','ENDPOINT_CLIENT','MISCELLANEOUS', IgnoreCase=$True)]
        [string]$Platform,
        [parameter(Mandatory=$False)]
        [ValidateSet('Events','Current','All', IgnoreCase=$True)]
        [string]$Source = 'All',    
        [parameter(Mandatory=$False)]
        [ValidateSet('LAST_HOUR','LAST_DAY','LAST_WEEK','LAST_MONTH','CUSTOM', IgnoreCase=$True)]
        [string]$TimePeriod = 'LAST_HOUR',
        [parameter(Mandatory=$False)]
        [Alias("StartDateFrom")]
        [string]$TimePeriodFrom,
        [parameter(Mandatory=$False)]
        [Alias("StartDateTo")]
        [string]$TimePeriodTo,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,100000)]
        [int]$PageSize = 1000,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,9999999)]
        [int]$ResultSize
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        $Params = @{
            'TimePeriod' = $TimePeriod
            'Platform' = $Platform
        }

        # Convert date to UNIX timestamp
        If($TimePeriodFrom) {
            $TimePeriodFrom = (Get-Date -Date $TimePeriodFrom -UFormat %s) + '000'
            $Params.Add('StartDateFrom',$TimePeriodFrom)
        }
        
        If($TimePeriodTo) {
            $TimePeriodTo = (Get-Date -Date $TimePeriodTo -UFormat %s) + '000'
            $Params.Add('StartDateTo',$TimePeriodTo)
        }
        
        Try {
        
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { 
                $TenantName = $Global:NectarTenantName 
            }
    
            If ($TenantName) { $Params.Add('Tenant',$TenantName) }

            $URI = "https://$Global:NectarCloud/dapi/platform/cluster/$ClusterID/summary"
            Write-Verbose $URI        
            
            $JSON = Invoke-RestMethod -Method GET -Credential $Global:NectarCred -uri $URI -Body $Params    
            
            If ($TenantName) {$JSON | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty}
            
            If ($Source -ne 'All') {
                $JSON.$Source
            }
            Else {
                $JSON
            }
        }
        Catch {
            Write-Error "Could not get platform item summary"
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



Function Get-NectarPlatformItemResources {
    <#
        .SYNOPSIS
        Return resource information about a specific platform item
         
        .DESCRIPTION
        Return resource information about a specific platform item
         
        .PARAMETER ClusterID
        The ID of a cluster to return resource information
         
        .PARAMETER Platform
        Show information about selected platform. Choose one or more from: 'AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','CISCO','CISCO_CMS','CISCO_VKM','SKYPE','SKYPE_ONLINE','TEAMS'
 
        .PARAMETER TimePeriod
        The time period to show event data from. Select from 'LAST_HOUR','LAST_DAY','LAST_WEEK','LAST_MONTH','CUSTOM'.
        CUSTOM requires using TimePeriodFrom and TimePeriodTo parameters.
 
        .PARAMETER TimePeriodFrom
        The earliest date/time to show event data from. Must be used in conjunction with -TimePeriod CUSTOM and TimePeriodTo parameters. Use format 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'. All time/dates in UTC. Use date-time format as in 2020-04-20T17:46:37.554
         
        .PARAMETER TimePeriodTo
        The latest date/time to show event data from. Must be used in conjunction with -TimePeriod CUSTOM and TimePeriodFrom parameters. Use format 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'. All time/dates in UTC. Use date-time format as in 2020-04-20T17:46:37.554
         
        .PARAMETER TenantName
        The name of the Nectar 10 tenant. Used in multi-tenant configurations.
         
        .PARAMETER PageSize
        The size of the page used to return data. Defaults to 1000
         
        .PARAMETER ResultSize
        The total number of results to return. Defaults to 1000. Maximum result size is 9,999,999 results
 
        .EXAMPLE
        Get-NectarPlatformItemResources -Platform CISCO -ClusterID 3_1
 
        .NOTES
        Version 1.0
    #>

    
    Param (
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [string]$ClusterID,
        [parameter(Mandatory=$True)]
        [ValidateSet('AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','SKYPE','CISCO','CISCO_CMS','CISCO_VKM','TEAMS','SKYPE_ONLINE','CISCO_CMS_VKM','AVAYA_AURA_CM','RIG','LYNC_VKM','SKYPE_FOR_BUSINESS_VKM','CISCO_UNITY','CISCO_EXPRESSWAY','AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','ZOOM','ENDPOINT_CLIENT','MISCELLANEOUS', IgnoreCase=$True)]
        [string]$Platform,
        [parameter(Mandatory=$False)]
        [ValidateSet('LAST_HOUR','LAST_DAY','LAST_WEEK','LAST_MONTH','CUSTOM', IgnoreCase=$True)]
        [string]$TimePeriod = 'LAST_HOUR',
        [parameter(Mandatory=$False)]
        [Alias("StartDateFrom")]
        [string]$TimePeriodFrom,
        [parameter(Mandatory=$False)]
        [Alias("StartDateTo")]
        [string]$TimePeriodTo,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,100000)]
        [int]$PageSize = 1000,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,9999999)]
        [int]$ResultSize
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        $Params = @{
            'TimePeriod' = $TimePeriod
            'Platform' = $Platform
        }

        # Convert date to UNIX timestamp
        If($TimePeriodFrom) {
            $TimePeriodFrom = (Get-Date -Date $TimePeriodFrom -UFormat %s) + '000'
            $Params.Add('StartDateFrom',$TimePeriodFrom)
        }
        
        If($TimePeriodTo) {
            $TimePeriodTo = (Get-Date -Date $TimePeriodTo -UFormat %s) + '000'
            $Params.Add('StartDateTo',$TimePeriodTo)
        }
        
        Try {
        
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { 
                $TenantName = $Global:NectarTenantName 
            }
    
            If ($TenantName) { $Params.Add('Tenant',$TenantName) }

            $URI = "https://$Global:NectarCloud/dapi/platform/cluster/$ClusterID/resources"
            Write-Verbose $URI        
            
            $JSON = Invoke-RestMethod -Method GET -Credential $Global:NectarCred -uri $URI -Body $Params    
            
            If ($TenantName) {$JSON | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty}
            
            $JSON
        }
        Catch {
            Write-Error "Could not get platform item server resources"
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



Function Get-NectarPlatformItemServers {
    <#
        .SYNOPSIS
        Return information about a specific platform item's servers
         
        .DESCRIPTION
        Return information about a specific platform item's servers
         
        .PARAMETER ClusterID
        The ID of a cluster to return server information
         
        .PARAMETER Platform
        Show information about selected platform. Choose one or more from: 'AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','CISCO','CISCO_CMS','CISCO_VKM','SKYPE','SKYPE_ONLINE','TEAMS'
 
        .PARAMETER Type
        Show information about either publishers, subscribers or both
         
        .PARAMETER TimePeriod
        The time period to show event data from. Select from 'LAST_HOUR','LAST_DAY','LAST_WEEK','LAST_MONTH','CUSTOM'.
        CUSTOM requires using TimePeriodFrom and TimePeriodTo parameters.
 
        .PARAMETER TimePeriodFrom
        The earliest date/time to show event data from. Must be used in conjunction with -TimePeriod CUSTOM and TimePeriodTo parameters. Use format 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'. All time/dates in UTC. Use date-time format as in 2020-04-20T17:46:37.554
         
        .PARAMETER TimePeriodTo
        The latest date/time to show event data from. Must be used in conjunction with -TimePeriod CUSTOM and TimePeriodFrom parameters. Use format 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'. All time/dates in UTC. Use date-time format as in 2020-04-20T17:46:37.554
         
        .PARAMETER TenantName
        The name of the Nectar 10 tenant. Used in multi-tenant configurations.
         
        .PARAMETER PageSize
        The size of the page used to return data. Defaults to 1000
         
        .PARAMETER ResultSize
        The total number of results to return. Defaults to 1000. Maximum result size is 9,999,999 results
 
        .EXAMPLE
        Get-NectarPlatformItemServers -Platform CISCO -ClusterID 3_1
 
        .NOTES
        Version 1.0
    #>

    
    Param (
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [string]$ClusterID,
        [parameter(Mandatory=$True)]
        [ValidateSet('AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','SKYPE','CISCO','CISCO_CMS','CISCO_VKM','TEAMS','SKYPE_ONLINE','CISCO_CMS_VKM','AVAYA_AURA_CM','RIG','LYNC_VKM','SKYPE_FOR_BUSINESS_VKM','CISCO_UNITY','CISCO_EXPRESSWAY','AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','ZOOM','ENDPOINT_CLIENT','MISCELLANEOUS', IgnoreCase=$True)]
        [string]$Platform,
        [parameter(Mandatory=$False)]
        [ValidateSet('Publisher','Subscribers','All', IgnoreCase=$True)]
        [string]$Type = 'All',    
        [parameter(Mandatory=$False)]
        [ValidateSet('LAST_HOUR','LAST_DAY','LAST_WEEK','LAST_MONTH','CUSTOM', IgnoreCase=$True)]
        [string]$TimePeriod = 'LAST_HOUR',
        [parameter(Mandatory=$False)]
        [Alias("StartDateFrom")]
        [string]$TimePeriodFrom,
        [parameter(Mandatory=$False)]
        [Alias("StartDateTo")]
        [string]$TimePeriodTo,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,100000)]
        [int]$PageSize = 1000,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,9999999)]
        [int]$ResultSize
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        $Params = @{
            'TimePeriod' = $TimePeriod
            'Platform' = $Platform
        }

        # Convert date to UNIX timestamp
        If($TimePeriodFrom) {
            $TimePeriodFrom = (Get-Date -Date $TimePeriodFrom -UFormat %s) + '000'
            $Params.Add('StartDateFrom',$TimePeriodFrom)
        }
        
        If($TimePeriodTo) {
            $TimePeriodTo = (Get-Date -Date $TimePeriodTo -UFormat %s) + '000'
            $Params.Add('StartDateTo',$TimePeriodTo)
        }
        
        Try {
        
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { 
                $TenantName = $Global:NectarTenantName 
            }
    
            If ($TenantName) { $Params.Add('Tenant',$TenantName) }

            $URI = "https://$Global:NectarCloud/dapi/platform/cluster/$ClusterID/servers"
            Write-Verbose $URI        
            
            $JSON = Invoke-RestMethod -Method GET -Credential $Global:NectarCred -uri $URI -Body $Params    
            
            If ($TenantName) {$JSON | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty}
            
            If ($Type -ne 'All') {
                $JSON.$Type
            }
            Else {
                $JSON
            }
        }
        Catch {
            Write-Error "Could not get platform item servers"
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



Function Get-NectarPlatformServerServices {
    <#
        .SYNOPSIS
        Return service information about a specific platform item
         
        .DESCRIPTION
        Return service information about a specific platform item
         
        .PARAMETER ClusterID
        The ID of a cluster to return service information
         
        .PARAMETER ServerID
        The ID of a server within a cluster to return service information
         
        .PARAMETER Platform
        Show information about selected platform. Choose one or more from: 'AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','CISCO','CISCO_CMS','CISCO_VKM','SKYPE','SKYPE_ONLINE','TEAMS'
 
        .PARAMETER TimePeriod
        The time period to show event data from. Select from 'LAST_HOUR','LAST_DAY','LAST_WEEK','LAST_MONTH','CUSTOM'.
        CUSTOM requires using TimePeriodFrom and TimePeriodTo parameters.
 
        .PARAMETER TimePeriodFrom
        The earliest date/time to show event data from. Must be used in conjunction with -TimePeriod CUSTOM and TimePeriodTo parameters. Use format 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'. All time/dates in UTC. Use date-time format as in 2020-04-20T17:46:37.554
         
        .PARAMETER TimePeriodTo
        The latest date/time to show event data from. Must be used in conjunction with -TimePeriod CUSTOM and TimePeriodFrom parameters. Use format 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'. All time/dates in UTC. Use date-time format as in 2020-04-20T17:46:37.554
         
        .PARAMETER TenantName
        The name of the Nectar 10 tenant. Used in multi-tenant configurations.
         
        .PARAMETER PageSize
        The size of the page used to return data. Defaults to 1000
         
        .PARAMETER ResultSize
        The total number of results to return. Defaults to 1000. Maximum result size is 9,999,999 results
 
        .EXAMPLE
        Get-NectarPlatformServerServices -Platform CISCO -ClusterID 3_1 -ServerID 1
 
        .NOTES
        Version 1.0
    #>

    
    Param (
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [Alias("PlatformItemId")]
        [string]$ClusterID,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [Alias("Id")]
        [string]$ServerID,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [ValidateSet('EDGE','FE','MEDIATION','GATEWAY','PUBLISHER','SUBSCRIBER','CUBE','IM_PRESENCE','VG224','CUCM','CMS','HW_CFB','CUCM_SW_CFB', IgnoreCase=$False)]
        [string]$Type,
        [parameter(Mandatory=$True)]
        [ValidateSet('AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','SKYPE','CISCO','CISCO_CMS','CISCO_VKM','TEAMS','SKYPE_ONLINE','CISCO_CMS_VKM','AVAYA_AURA_CM','RIG','LYNC_VKM','SKYPE_FOR_BUSINESS_VKM','CISCO_UNITY','CISCO_EXPRESSWAY','AVAYA_MEDIA_GATEWAY','AVAYA_SESSION_MANAGER','AVAYA_VOICE_PORTAL','ZOOM','ENDPOINT_CLIENT','MISCELLANEOUS', IgnoreCase=$False)]
        [string]$Platform,
        [parameter(Mandatory=$False)]
        [ValidateSet('LAST_HOUR','LAST_DAY','LAST_WEEK','LAST_MONTH','CUSTOM', IgnoreCase=$False)]
        [string]$TimePeriod = 'LAST_HOUR',
        [parameter(Mandatory=$False)]
        [Alias("StartDateFrom")]
        [string]$TimePeriodFrom,
        [parameter(Mandatory=$False)]
        [Alias("StartDateTo")]
        [string]$TimePeriodTo,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,100000)]
        [int]$PageSize = 1000,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,9999999)]
        [int]$ResultSize
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        $Params = @{
            'TimePeriod' = $TimePeriod
            'Platform' = $Platform
            'type' = $Type
        }

        # Convert date to UNIX timestamp
        If($TimePeriodFrom) {
            $TimePeriodFrom = (Get-Date -Date $TimePeriodFrom -UFormat %s) + '000'
            $Params.Add('StartDateFrom',$TimePeriodFrom)
        }
        
        If($TimePeriodTo) {
            $TimePeriodTo = (Get-Date -Date $TimePeriodTo -UFormat %s) + '000'
            $Params.Add('StartDateTo',$TimePeriodTo)
        }
        
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { 
                $TenantName = $Global:NectarTenantName 
            }
    
            If ($TenantName) { $Params.Add('Tenant',$TenantName) }

            $URI = "https://$Global:NectarCloud/dapi/platform/cluster/$ClusterID/server/$ServerID/services"
            Write-Verbose $URI        
            
            $JSON = Invoke-RestMethod -Method GET -Credential $Global:NectarCred -uri $URI -Body $Params    
            
            If ($TenantName) {$JSON | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty}
            
            $JSON
        }
        Catch {
            Write-Error "Could not get platform item server services"
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}




#################################################################################################################################################
# #
# Endpoint Client Functions #
# #
#################################################################################################################################################

Function Get-NectarEndpoint {
    <#
        .SYNOPSIS
        Returns a list of Nectar 10 endpoints
 
        .DESCRIPTION
        Returns a list of Nectar 10 endpoints
 
        .PARAMETER SearchQuery
        A string to search for. Will search for match against all fields
         
        .PARAMETER OrderByField
        Sort the output by the selected field
         
        .PARAMETER OrderDirection
        Sort ordered output in ascending or descending order
         
        .PARAMETER TenantName
        The name of the Nectar 10 tenant. Used in multi-tenant configurations.
         
        .PARAMETER PageSize
        The size of the page used to return data. Defaults to 1000
         
        .PARAMETER ResultSize
        The total number of results to return. Defaults to 100000. Maximum result size is 9,999,999 results
         
        .EXAMPLE
        Get-NectarEndpoint
        Returns the first 100000 endpoints
         
        .EXAMPLE
        Get-NectarEndpoint -ResultSize 100
        Returns the first 100 endpoints
 
        .EXAMPLE
        Get-NectarEndpoint -SearchQuery US
        Returns all endpoints that have US in any of the data fields
 
        .NOTES
        Version 1.0
    #>

    
    [Alias("gne")]    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$SearchQuery,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateSet('id','uuid','name','ipAddress','isHub','mapped','userName','userDisplayName','location', IgnoreCase=$False)]
        [string]$OrderByField = 'id',
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateSet('asc','desc', IgnoreCase=$False)]
        [string]$OrderDirection = 'asc',
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,100000)]
        [int]$PageSize = 1000,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,100000)]
        [int]$ResultSize = 100000
    )
    
    Begin {
        Connect-NectarCloud
    }        
    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
            
            $URI = "https://$Global:NectarCloud/aapi/testing/entities/"
            
            $Params = @{
                'orderByField' = $OrderByField
                'orderDirection' = $OrderDirection
            }
            
            If ($SearchQuery) { $Params.Add('searchQuery', $SearchQuery) }
            
            If ($ResultSize) { 
                $Params.Add('pageSize', $ResultSize) 
            }
            Else { 
                $Params.Add('pageSize', $PageSize)
            }

            If ($TenantName) { $Params.Add('tenant', $TenantName) }            
            

            $JSON = Invoke-RestMethod -Method GET -Credential $Global:NectarCred -uri $URI -Body $Params
            If ($TenantName) { $JSON.elements | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty } # Add the tenant name to the output which helps pipelining
            $JSON.elements
            
            $TotalPages = $JSON.totalPages
        
            If ($TotalPages -gt 1 -and !($ResultSize)) {
                $PageNum = 2
                Write-Verbose "Page size: $PageSize"
                While ($PageNum -le $TotalPages) {
                    Write-Verbose "Working on page $PageNum of $TotalPages"
                    $PagedURI = $URI + "?pageNumber=$PageNum"
                    $JSON = Invoke-RestMethod -Method GET -Credential $Global:NectarCred -uri $PagedURI -Body $Params
                    If ($TenantName) { $JSON.elements | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty }
                    $JSON.elements
                    $PageNum++
                }
            }

        }
        Catch {
            Write-Error "Unable to retrieve endpoint information"
            Get-JSONErrorStream -JSONResponse $_        
        }
    }
}



Function Set-NectarEndpoint {
    <#
        .SYNOPSIS
        Modify properties of a Nectar 10 endpoint
 
        .DESCRIPTION
        Modify properties of a Nectar 10 endpoint
 
        .PARAMETER SearchQuery
        A string to search for to return a specific endpoint. Will search for match against all fields
         
        .PARAMETER UserName
        The username to associate with the given endpoint. Set to $NULL to de-associate a user with an endpoint
         
        .PARAMETER IsHub
        Set the endpoint to be a hub endpoint or not.
         
        .PARAMETER TenantName
        The name of the Nectar 10 tenant. Used in multi-tenant configurations.
         
        .EXAMPLE
        Set-NectarEndpoint -SearchQuery ff4bf84a-04d8-11ec-ad2e-17d4f8d1df89 -UserName tferguson@contoso.com -IsHub $False
        Sets a specific endpoint's associated username to 'tferguson'
         
        .NOTES
        Version 1.0
    #>

    
    [Alias("gne")]    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [Alias('UUID')]
        [string]$SearchQuery,
        [Parameter(Mandatory=$False)]
        [string]$UserName,
        [Parameter(Mandatory=$False)]
        [string]$DisplayName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [bool]$IsHub,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,100000)]
        [int]$PageSize = 1000,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,100000)]
        [int]$ResultSize = 100000
    )
    
    Begin {
        Connect-NectarCloud
    }        
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
        
        $ECSearchURI = "https://$Global:NectarCloud/aapi/testing/entities/"
        $ECUpdateURI = "https://$Global:NectarCloud/aapi/testing/entity/"
        $UserNameURI = "https://$Global:NectarCloud/aapi/testing/entities/usernames/"
        
        Try {    

            # Search for a single valid endpoint. Throw error if zero or more than 1 endpoint returned
            $ECSearchParams = @{
                'searchQuery' = $SearchQuery
            }
            
            If ($TenantName) { $ECSearchParams.Add('tenant', $TenantName) }

            $ECSearchJSON = Invoke-RestMethod -Method GET -Credential $Global:NectarCred -uri $ECSearchURI -Body $ECSearchParams
            
            If ($ECSearchJSON.totalElements -gt 1) {
                Write-Error 'Too many endpoints returned. Please refine your search query to return only a single endpoint.'
                Return
            }
            ElseIf ($ECSearchJSON.totalElements -eq 0) {
                Write-Error "Could not find endpoint $SearchQuery"
                Return
            }
            
            $EndpointDetails = $ECSearchJSON.elements
            
            # Search for a single valid username. Throw error if zero or more than 1 username returned
            If ($UserName) {
                $UserNameParams = @{
                    'searchQuery' = $UserName
                }
                
                If ($TenantName) { $UserNameParams.Add('tenant', $TenantName) }

                $UserSearchJSON = Invoke-RestMethod -Method GET -Credential $Global:NectarCred -uri $UserNameURI -Body $UserNameParams
                
                If ($UserSearchJSON.Count -gt 1) {
                    Write-Error 'Too many usernames returned. Please refine your search query to return only a single username.'
                    Return
                }
                ElseIf ($UserSearchJSON.Count -eq 0) {
                    Write-Error "Could not find user with name $UserName"
                    Return
                }
                
                $UserName = $UserSearchJSON.UserName
            }
            
            If ($PSBoundParameters.Keys.Contains('UserName')) {
                If ($UserName) {
                    $EndpointDetails[0].userName = $UserName
                }
                Else {
                    $EndpointDetails[0].userName = $NULL
                }
            }
            
            If ($DisplayName) { $EndpointDetails[0].userDisplayName = $DisplayName }

            If ($PSBoundParameters.Keys.Contains('IsHub')) { 
                $EndpointDetails[0].IsHub = $IsHub 
                $EndpointDetails[0].userName = $NULL
            }
            
            $EndpointJSON = $EndpointDetails | ConvertTo-Json
            
            Write-Verbose $EndpointJSON
            
            If ($TenantName) { $ECUpdateURI = $ECUpdateURI + "?tenant=$TenantName" }
            
            $EndpointUpdate = Invoke-RestMethod -Method PUT -Credential $Global:NectarCred -uri $ECUpdateURI -Body $EndpointJSON -ContentType 'application/json; charset=utf-8'
        }
        Catch {
            Write-Error "Unable to set endpoint information"
            Get-JSONErrorStream -JSONResponse $_        
        }
    }
}




Function Get-NectarEndpointTests {
    <#
        .SYNOPSIS
        Returns information about Endpoint Client tests
 
        .DESCRIPTION
        Returns information about Endpoint Client tests
        UI_ELEMENT
         
        .PARAMETER TimePeriod
        The time period to show session data from. Select from 'LAST_HOUR','LAST_DAY','LAST_WEEK','LAST_MONTH','CUSTOM'.
        CUSTOM requires using TimePeriodFrom and TimePeriodTo parameters.
         
        .PARAMETER TimePeriodFrom
        The earliest date/time to show session data from. Must be used in conjunction with -TimePeriod CUSTOM and TimePeriodTo parameters. Use format 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'. All time/dates in UTC.
         
        .PARAMETER TimePeriodTo
        The latest date/time to show session data from. Must be used in conjunction with -TimePeriod CUSTOM and TimePeriodFrom parameters. Use format 'YYYY-MM-DD' or 'YYYY-MM-DD HH:MM:SS'. All time/dates in UTC.
         
        .PARAMETER TestTypes
        The types of EPC tests to return. Choose from one or more of 'P2P', 'PING', 'AUDIO', or 'VIDEO'
         
        .PARAMETER TestResults
        The result type to return. Choose from one or more of 'PASSED','FAILED', or 'UNKNOWN'
         
        .PARAMETER ResponseCodes
        Show tests that match one or more SIP response codes. Accepts numbers from 0 to 699
         
        .PARAMETER Locations
        Show sessions where the selected location was used by either caller or callee. Can query for multiple locations.
 
        .PARAMETER CallerLocations
        Show sessions where the selected location was used by the caller. Can query for multiple locations.
         
        .PARAMETER CalleeLocations
        Show sessions where the selected location was used by the callee. Can query for multiple locations.
         
        .PARAMETER ExtCities
        Show sessions where the caller or callee was located in the selected city (as detected via geolocating the user's external IP address). Can query for multiple cities.
 
        .PARAMETER CallerExtCities
        Show sessions where the caller was located in the selected city (as detected via geolocating the user's external IP address). Can query for multiple cities.
         
        .PARAMETER CalleeExtCities
        Show sessions where the callee was located in the selected city (as detected via geolocating the user's external IP address). Can query for multiple cities.
         
        .PARAMETER ExtCountries
        Show sessions where the caller or callee was located in the selected country (as detected via geolocating the user's external IP address). Can query for multiple countries.
 
        .PARAMETER CallerExtCountries
        Show sessions where the caller was located in the selected country (as detected via geolocating the user's external IP address). Can query for multiple countries.
         
        .PARAMETER CalleeExtCountries
        Show sessions where the callee was located in the selected country (as detected via geolocating the user's external IP address). Can query for multiple countries.
         
        .PARAMETER ExtISPs
        Show sessions where the caller or callee was located in the selected ISP (as detected via geolocating the user's external IP address). Can query for multiple ISPs.
 
        .PARAMETER CallerExtISPs
        Show sessions where the caller was located in the selected ISP (as detected via geolocating the user's external IP address). Can query for multiple ISPs.
         
        .PARAMETER CalleeExtISPs
        Show sessions where the callee was located in the selected ISP (as detected via geolocating the user's external IP address). Can query for multiple ISPs.
         
        .PARAMETER Users
        Show sessions where the selected user was either caller or callee. Can query for multiple users.
 
        .PARAMETER FromUsers
        Show sessions where the selected user was the caller. Can query for multiple users.
         
        .PARAMETER ToUsers
        Show sessions where the selected user was the callee. Can query for multiple users.
         
        .PARAMETER OrderByField
        Sort the output by the selected field
         
        .PARAMETER OrderDirection
        Sort direction. Use with OrderByField. Not case sensitive. Choose from:
        ASC, DESC
 
        .PARAMETER TenantName
        The name of the Nectar 10 tenant. Used in multi-tenant configurations.
         
        .PARAMETER PageSize
        The size of the page used to return data. Defaults to 1000
         
        .PARAMETER ResultSize
        The total number of results to return. Defaults to 1000. Maximum result size is 9,999,999 results
         
        .EXAMPLE
        Get-NectarSessions -TimePeriod LAST_HOUR -Platforms TEAMS -Modalities AUDIO -SessionQualities POOR
        Returns a list of all Teams audio sessions for the past hour where the quality was rated Poor
         
        .EXAMPLE
        (Get-NectarSessions -SessionTypes CONFERENCE -TimePeriod CUSTOM -TimePeriodFrom '2021-05-06' -TimePeriodTo '2021-05-07').Count
        Returns a count of all conferences between May 6 and 7 (all times/dates UTC)
         
        .EXAMPLE
        Get-NectarSessions -SessionTypes PEER2PEER,PEER2PEER_MULTIMEDIA -TimePeriod CUSTOM -TimePeriodFrom '2021-05-06 14:00' -TimePeriodTo '2021-05-06 15:00'
        Returns a list of all P2P calls between 14:00 and 15:00 UTC on May 6
 
        .EXAMPLE
        Get-NectarSessions -TimePeriod LAST_WEEK -SessionTypes CONFERENCE | Select-Object confOrganizerOrSpace | Group-Object confOrganizerOrSpace | Select-Object Name, Count | Sort-Object Count -Descending
        Returns a list of conference organizers and a count of the total conferences organized by each, sorted by count.
         
        .NOTES
        Version 1.0
    #>

    
    [CmdletBinding(PositionalBinding=$False)]
    Param (
        [parameter(Mandatory=$False)]
        [ValidateSet('LAST_HOUR','LAST_DAY','LAST_WEEK','LAST_MONTH','CUSTOM', IgnoreCase=$True)]
        [string]$TimePeriod = 'LAST_HOUR',
        [parameter(Mandatory=$False)]
        [Alias("StartDateFrom")]
        [string]$TimePeriodFrom,
        [parameter(Mandatory=$False)]
        [Alias("StartDateTo")]
        [string]$TimePeriodTo,
        [parameter(Mandatory=$False)]
        [ValidateSet('P2P','PING','AUDIO','VIDEO', IgnoreCase=$False)]
        [string[]]$TestTypes,    
        [parameter(Mandatory=$False)]
        [ValidateSet('PASSED','FAILED','UNKNOWN', IgnoreCase=$False)]
        [string[]]$TestResults,
        [Parameter(Mandatory=$False)]
        [ValidateRange(0,699)]
        [string[]]$ResponseCodes,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$Users,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$FromUsers,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ToUsers,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$Locations,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerLocations,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeLocations,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ExtCities,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerExtCities,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeExtCities,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ExtCountries,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerExtCountries,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeExtCountries,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$ExtISPs,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CallerExtISPs,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string[]]$CalleeExtISPs,
        [parameter(Mandatory=$False)]
        [string]$OrderByField,
        [parameter(Mandatory=$False)]
        [ValidateSet('ASC','DESC', IgnoreCase=$True)]
        [string]$OrderDirection,
        [parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,10000)]
        [int]$PageSize = 1000,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,50000)]
        [int]$ResultSize
    )
    
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
        
        If ($PageSize) { $PSBoundParameters.Remove('PageSize') | Out-Null }
        If ($ResultSize) { $PSBoundParameters.Remove('ResultSize') | Out-Null }
        If ($OrderByField) { $PSBoundParameters.Remove('OrderByField') | Out-Null }
        If ($OrderDirection) { $PSBoundParameters.Remove('OrderDirection') | Out-Null }
        
        $FilterSession = Set-NectarFilterParams @PsBoundParameters -Platform ENDPOINT_CLIENT -Scope ENDPOINT_CLIENT
        
        $URI = "https://$Global:NectarCloud/dapi/testing/results"
        
        # Set the page size to the result size if -ResultSize switch is used to limit the number of returned items
        # Otherwise, set page size (defaults to 1000)
        $Params = @{
            'Scope' = 'ENDPOINT_CLIENT'
            'Platform' = 'ENDPOINT_CLIENT'
        }
        
        If ($ResultSize) {
            $Params.Add( 'pageSize',$ResultSize)
        }
        Else {
            $Params.Add('pageSize',$PageSize)
        }
        
        If($OrderByField) { $Params.Add('orderByField',$OrderByField) }
        If($OrderDirection) { $Params.Add('orderDirection',$OrderDirection) }
        If ($TenantName) { $Params.Add('tenant',$TenantName) }
        
        # Return results in pages
        Try {
            Write-Verbose $URI
            foreach($k in $Params.Keys){Write-Verbose "$k $($Params[$k])"}
            $JSON = Invoke-RestMethod -Method GET -uri $URI -Body $Params -WebSession $FilterSession
            
            $TotalPages = $JSON.totalPages
            
            If ($TenantName) {$JSON.elements | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty}
            $JSON.elements | Add-Member -TypeName 'Nectar.SessionList'
            $JSON.elements
            
            If ($TotalPages -gt 1 -and !($ResultSize)) {
                $PageNum = 2
                Write-Verbose "Page size: $PageSize"
                While ($PageNum -le $TotalPages) {
                    Write-Verbose "Working on page $PageNum of $TotalPages"
                    $PagedURI = $URI + "?pageNumber=$PageNum"
                    $JSON = Invoke-RestMethod -Method GET -uri $PagedURI -Body $Params -WebSession $FilterSession
                    If ($TenantName) {$JSON.elements | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty}
                    $JSON.elements
                    $PageNum++
                }
            }
        }
        Catch {
            Write-Error "No results. Try specifying a less-restrictive filter"
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}





#################################################################################################################################################
# #
# MS Teams Functions #
# #
#################################################################################################################################################

Function Get-MSGraphAccessToken {
    <#
        .SYNOPSIS
        Get a Microsoft Graph access token for a given MS tenant. Needed to run other Graph API queries.
         
        .DESCRIPTION
        Get a Microsoft Graph access token for a given MS tenant. Needed to run other Graph API queries.
 
        .PARAMETER MSClientID
        The MS client ID for the application granted access to Azure AD.
         
        .PARAMETER MSClientSecret
        The MS client secret for the application granted access to Azure AD.
         
        .PARAMETER MSTenantID
        The MS tenant ID for the O365 customer granted access to Azure AD.
         
        .PARAMETER CertFriendlyName
        The friendly name of an installed certificate to be used for certificate authentication. Can be used instead of MSClientSecret
 
        .PARAMETER CertThumbprint
        The thumbprint of an installed certificate to be used for certificate authentication. Can be used instead of MSClientSecret
 
        .PARAMETER CertPath
        The path to a PFX certificate to be used for certificate authentication. Can be used instead of MSClientSecret
         
        .PARAMETER CertStore
        The certificate store to be used for certificate authentication. Select either LocalMachine or CurrentUser. Used in conjunction with CertThumbprint or CertFriendlyName
        Can be used instead of MSClientSecret.
         
        .EXAMPLE
        $AuthToken = Get-MSGraphAccessToken -MSClientID 41a228ad-db6c-4e4e-4184-6d8a1175a35f -MSClientSecret 43Rk5Xl3K349w-pFf0i_Rt45Qd~ArqkE32. -MSTenantID 17e1e614-8119-48ab-8ba1-6ff1d94a6930
        Obtains an authtoken for the given tenant using secret-based auth and saves the results for use in other commands in a variable called $AuthToken
         
        .EXAMPLE
        $AuthToken = Get-MSGraphAccessToken -MSClientID 029834092-234234-234234-23442343 -MSTenantID 234234234-234234-234-23-42342342 -CertFriendlyName 'CertAuth' -CertStore LocalMachine
        Obtains an authtoken for the given tenant using certificate auth and saves the results for use in other commands in a variable called $AuthToken
 
        .NOTES
        Version 1.0
    #>

    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [string]$MSClientID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$MSClientSecret,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [string]$MSTenantID,
        [Parameter(Mandatory=$False)]
        [switch]$N10Cert,
        [Parameter(Mandatory=$False)]
        [string]$CertFriendlyName,
        [Parameter(Mandatory=$False)]
        [string]$CertThumbprint,
        [Parameter(Mandatory=$False)]
        [string]$CertPath,
        [Parameter(Mandatory=$False)]
        [ValidateSet('LocalMachine','CurrentUser', IgnoreCase=$True)]
        [string]$CertStore = 'CurrentUser'
    )
    
    Begin {
        $Scope = 'https://graph.microsoft.com/.default'
    }
    Process {
        If ($MSClientSecret) {
            Try {
                # Get the Azure Graph API auth token
                $AuthBody = @{
                    grant_type = 'client_credentials'
                    client_id = $MSClientID
                    client_secret = $MSClientSecret
                    scope = $Scope
                }
            
                $JSON_Auth = Invoke-RestMethod -Method POST -uri "https://login.microsoftonline.com/$MSTenantID/oauth2/v2.0/token" -Body $AuthBody
                $AuthToken = $JSON_Auth.access_token
                
                Return $AuthToken
            }
            Catch {
                Write-Error "Failed to get access token. Ensure the values for MSTenantID, MSClientID and MSClientSecret are correct."
                Get-JSONErrorStream -JSONResponse $_
            }
        }
        Else {
            # Needs access to the full certificate stored in Nectar 10, and can be exported to PEM via Get-NectarMSTeamsSubscription (only if you have global admin privs)
            # Get-NectarMSTeamsSubscription -ExportCertificate
            
            # Need to create a certificate from the resulting PEM files using the following command:
            # .\openssl.exe pkcs12 -export -in D:\OneDrive\Nectar\Scripts\TeamsCert.pem -inkey D:\OneDrive\Nectar\Scripts\TeamsPriv.key -CSP "Microsoft Enhanced RSA and AES Cryptographic Provider" -out D:\OneDrive\Nectar\Scripts\FullCert.pfx
            # Requires that OpenSSL is installed
            
            # Then use the resulting certificate to obtain an access token:
            # $GraphToken = Get-MSGraphAccessToken -MSTenantID <tenantID> -MSClientID <Client/AppID> -CertPath .\FullCert.pfx
            
            # Then use the resulting token in other commands like:
            # Test-MSTeamsConnectivity -AuthToken $GraphToken
                        
            # Get the certificate information via one of several methods
            If ($CertThumbprint) { $Certificate = Get-Item Cert:\$CertStore\My\$CertThumbprint }
            If ($CertFriendlyName) { $Certificate = Get-ChildItem Cert:\$CertStore\My | Where {$_.FriendlyName -eq $CertFriendlyName} }
            If ($CertPath) { $Certificate = Get-PfxCertificate -FilePath $CertPath }
            If ($N10Cert) { 
                # Get certificate BASE64 encoding from N10
                $CertBlob = (Get-NectarMSTeamsSubscription).msClientCertificateDto.certificate
                $KeyBlob = (Get-NectarMSTeamsSubscription).msClientCertificateDto.privateKey
                $CertRaw = $CertBlob -replace "-----BEGIN CERTIFICATE-----", $NULL -replace "-----END CERTIFICATE-----", $NULL
                $KeyRaw = $KeyBlob -replace "-----BEGIN PRIVATE KEY-----", $NULL -replace "-----END PRIVATE KEY-----", $NULL
                $CertConv = [Convert]::FromBase64String($CertRaw)
                $KeyConv = [Convert]::FromBase64String($KeyRaw)
                $Certificate = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2
                $Certificate.Import([Convert]::FromBase64String($CertRaw))            
            }
            
            If ($Certificate) {
                # Adapted from https://adamtheautomator.com/microsoft-graph-api-powershell/
                # Create base64 hash of certificate
                $CertificateBase64Hash = [System.Convert]::ToBase64String($Certificate.GetCertHash())
                
                # Create JWT timestamp for expiration
                $StartDate = (Get-Date "1970-01-01T00:00:00Z" ).ToUniversalTime()
                $JWTExpirationTimeSpan = (New-TimeSpan -Start $StartDate -End (Get-Date).ToUniversalTime().AddMinutes(2)).TotalSeconds
                $JWTExpiration = [math]::Round($JWTExpirationTimeSpan,0)

                # Create JWT validity start timestamp
                $NotBeforeExpirationTimeSpan = (New-TimeSpan -Start $StartDate -End ((Get-Date).ToUniversalTime())).TotalSeconds
                $NotBefore = [math]::Round($NotBeforeExpirationTimeSpan,0)

                # Create JWT header
                $JWTHeader = @{
                    alg = "RS256"
                    typ = "JWT"
                    # Use the CertificateBase64Hash and replace/strip to match web encoding of base64
                    x5t = $CertificateBase64Hash -replace '\+','-' -replace '/','_' -replace '='
                }

                # Create JWT payload
                $JWTPayload = @{
                    # What endpoint is allowed to use this JWT
                    aud = "https://login.microsoftonline.com/$MSTenantID/oauth2/token"

                    # Expiration timestamp
                    exp = $JWTExpiration

                    # Issuer = your application
                    iss = $MSClientID

                    # JWT ID: random guid
                    jti = [guid]::NewGuid()

                    # Not to be used before
                    nbf = $NotBefore

                    # JWT Subject
                    sub = $MSClientID
                }

                # Convert header and payload to base64
                $JWTHeaderToByte = [System.Text.Encoding]::UTF8.GetBytes(($JWTHeader | ConvertTo-Json))
                $EncodedHeader = [System.Convert]::ToBase64String($JWTHeaderToByte)

                $JWTPayloadToByte =  [System.Text.Encoding]::UTF8.GetBytes(($JWTPayload | ConvertTo-Json))
                $EncodedPayload = [System.Convert]::ToBase64String($JWTPayloadToByte)

                # Join header and Payload with "." to create a valid (unsigned) JWT
                $JWT = $EncodedHeader + "." + $EncodedPayload

                # Get the private key object of your certificate
                $PrivateKey = $Certificate.PrivateKey
                
                # Define RSA signature and hashing algorithm
                $RSAPadding = [Security.Cryptography.RSASignaturePadding]::Pkcs1
                $HashAlgorithm = [Security.Cryptography.HashAlgorithmName]::SHA256 

                # Create a signature of the JWT
                # Breaks down here
                $Signature = [Convert]::ToBase64String($PrivateKey.SignData([System.Text.Encoding]::UTF8.GetBytes($JWT),$HashAlgorithm,$RSAPadding)) -replace '\+','-' -replace '/','_' -replace '='

                # Join the signature to the JWT with "."
                $JWT = $JWT + "." + $Signature
                
                # Create a hash with body parameters
                $Body = @{
                    client_id = $MSClientID
                    client_assertion = $JWT
                    client_assertion_type = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
                    scope = $Scope
                    grant_type = "client_credentials"
                }

                $Uri = "https://login.microsoftonline.com/$MSTenantID/oauth2/v2.0/token"

                # Use the self-generated JWT as Authorization
                $Header = @{
                    Authorization = "Bearer $JWT"
                }

                # Splat the parameters for Invoke-Restmethod for cleaner code
                $PostSplat = @{
                    ContentType = 'application/x-www-form-urlencoded'
                    Method = 'POST'
                    Body = $Body
                    Uri = $Uri
                    Headers = $Header
                }

                $Request = Invoke-RestMethod @PostSplat
                
                $AuthToken = $Request.access_token
                Return $AuthToken
            }
            Else {
                Write-Error "Could not find certificate in $CertStore"
            }
        }
    }
}



Function Get-MSOfficeAccessToken {
    <#
        .SYNOPSIS
        Get an Office 365 access token for a given MS tenant. Needed to run specific Office365 API queries (not Graph).
         
        .DESCRIPTION
        Get an access token for a given MS tenant. Needed to run specific Office365 API queries (not Graph).
 
        .EXAMPLE
        $MSOAccessToken = Get-MSOfficeAccessToken -MSClientID 41a228ad-db6c-4e4e-4184-6d8a1175a35f -MSClientSecret 43Rk5Xl3K349w-pFf0i_Rt45Qd~ArqkE32. -MSTenantID 17e1e614-8119-48ab-8ba1-6ff1d94a6930
        Obtains an MS Office access token using the supplied clientID/secret/tenantID and stores the results for later use.
 
        .NOTES
        Version 1.0
    #>

    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [string]$MSClientID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [string]$MSClientSecret,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [string]$MSTenantID
    )
    
    Process {
        Try {
            # Get the Azure Graph API auth token
            $AuthBody = @{
                grant_type = 'client_credentials'
                client_id = $MSClientID
                client_secret = $MSClientSecret
                resource = 'https://manage.office.com'
            }
        
            $JSON_Auth = Invoke-RestMethod -Method POST -uri "https://login.microsoftonline.com/$MSTenantID/oauth2/token?api-version=1.0" -Body $AuthBody
            $AuthToken = $JSON_Auth.access_token
            
            Return $AuthToken
        }
        Catch {
            Write-Error "Failed to get access token."
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}


Function Test-MSTeamsConnectivity {
    <#
        .SYNOPSIS
        Tests if we are able to retrieve Teams call data and Azure AD information from a O365 tenant.
         
        .DESCRIPTION
        Tests if we are able to retrieve Teams call data and Azure AD information from a O365 tenant.
 
        .PARAMETER MSClientID
        The MS client ID for the application granted access to Azure AD.
         
        .PARAMETER MSClientSecret
        The MS client secret for the application granted access to Azure AD.
         
        .PARAMETER MSTenantID
        The MS tenant ID for the O365 customer granted access to Azure AD.
                 
        .PARAMETER SkipUserCount
        Skips the user count
         
        .PARAMETER TenantName
        The name of the Nectar 10 tenant. Used in multi-tenant configurations.
         
        .PARAMETER AuthToken
        The authorization token used for this request. Normally obtained via Get-MSGraphAccessToken
 
        .EXAMPLE
        Get-NectarMSTeamsSubscription -TenantName contoso | Test-MSAzureADAccess
 
        .NOTES
        Version 1.1
    #>

    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$MSClientID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$MSClientSecret,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$MSTenantID,
        [Parameter(Mandatory=$False)]
        [switch]$SkipUserCount,    
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$AuthToken        
    )
    
    Process {
        If ($MSTenantID) { 
            $AuthToken = Get-MSGraphAccessToken -MSClientID $MSClientID -MSClientSecret $MSClientSecret -MSTenantID $MSTenantID
        }
        ElseIf (!$AuthToken) {
            $AuthToken = Get-NectarMSTeamsSubscription -TenantName $TenantName | Get-MSGraphAccessToken
        }
    
        $Headers = @{
            Authorization = "Bearer $AuthToken"
        }
        
        # Test MS Teams call record access
        Try {
            $FromDateTime = (Get-Date -Format 'yyyy-MM-dd')
            $ToDateTime = ((Get-Date).AddDays(+1).ToString('yyyy-MM-dd'))
            $URI = "https://graph.microsoft.com/beta/communications/callRecords/getDirectRoutingCalls(fromDateTime=$FromDateTime,toDateTime=$ToDateTime)"
            
            If ($TenantName) { Write-Host "TenantName: $TenantName - " -NoNewLine }
            Write-Host 'Teams CR Status: ' -NoNewLine
        
            $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Headers

            Write-Host 'PASS' -ForegroundColor Green
        }
        Catch {
            Write-Host 'FAIL' -ForegroundColor Red
        }

        # Test Azure AD user access
        Try {
            $URI = 'https://graph.microsoft.com/v1.0/users'
            
            If ($TenantName) { Write-Host "TenantName: $TenantName - " -NoNewLine }
            Write-Host 'Azure AD User Status: ' -NoNewLine
        
            $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Headers
            Write-Host 'PASS' -ForegroundColor Green
        }
        Catch {
            Write-Host 'FAIL' -ForegroundColor Red
            Get-JSONErrorStream -JSONResponse $_
        }

        # Test Azure AD group access
        Try {
            $URI = 'https://graph.microsoft.com/v1.0/groups'
            
            If ($TenantName) { Write-Host "TenantName: $TenantName - " -NoNewLine }
            Write-Host 'Azure AD Group Status: ' -NoNewLine
        
            $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Headers

            Write-Host 'PASS' -ForegroundColor Green
        }
        Catch {
            Write-Host 'FAIL' -ForegroundColor Red
        }
        If (!$SkipUserCount) { Get-MSTeamsUserLicenseCount -AuthToken $AuthToken }
        
        Clear-Variable AuthToken
    }
}




Function Get-MSAzureGraphSubscriptions {
    <#
        .SYNOPSIS
        Return data about the subscriptions for a given Azure tenant.
         
        .DESCRIPTION
        Return data about the subscriptions for a given Azure tenant.
         
        .PARAMETER MSClientID
        The MS client ID for the application granted access to Azure AD.
         
        .PARAMETER MSClientSecret
        The MS client secret for the application granted access to Azure AD.
         
        .PARAMETER MSTenantID
        The MS tenant ID for the O365 customer granted access to Azure AD.
         
        .PARAMETER TenantName
        The name of the Nectar 10 tenant. Used in multi-tenant configurations.
         
        .PARAMETER AuthToken
        The authorization token used for this request. Normally obtained via Get-MSGraphAccessToken
 
        .EXAMPLE
        Get-MSAzureGraphSubscriptions
 
        .NOTES
        Version 1.0
    #>

    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$MSClientID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$MSClientSecret,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$MSTenantID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$AuthToken    
    )
    
    Process {
        Try {
            If ($MSTenantID) { 
                $AuthToken = Get-MSGraphAccessToken -MSClientID $MSClientID -MSClientSecret $MSClientSecret -MSTenantID $MSTenantID
            }
            ElseIf (!$AuthToken) {
                $AuthToken = Get-NectarMSTeamsSubscription -TenantName $TenantName | Get-MSGraphAccessToken
            }

            # Test Azure AD access
            $Headers = @{
                Authorization = "Bearer $AuthToken"
            }
            
            $JSON = Invoke-RestMethod -Method GET -uri "https://graph.microsoft.com/v1.0/subscriptions" -Headers $Headers
            Return $JSON.value
        }
        Catch {
            Write-Error "Could not obtain subscription information."
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}




Function Get-NectarMSTeamsSubscription {
    <#
        .SYNOPSIS
        Return data about the MSTeams subscription for a given tenant.
         
        .DESCRIPTION
        Return data about the MSTeams subscription for a given tenant. Requires a global admin account. Not available to tenant-level admins.
 
        .EXAMPLE
        Get-NectarMSTeamsSubscription
 
        .NOTES
        Version 1.0
    #>

    
    [Alias("gnmtai","Get-MSTeamsAppInfo")]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [switch]$ExportCertificate,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        Try {
            If (!$TenantName) { $TenantName = Get-NectarDefaultTenantName }

            $JSON = Invoke-RestMethod -Method GET -Credential $Global:NectarCred -uri "https://$Global:NectarCloud/aapi/clouddatasources/configuration/msteams/subscription?tenant=$TenantName"
            
            If ($ExportCertificate) {
                Try {
                    $JSON.data.msClientCertificateDto.certificate | Out-File TeamsCert.pem -Encoding ASCII
                    $JSON.data.msClientCertificateDto.privateKey | Out-File TeamsPriv.key -Encoding ASCII
                    Write-Host 'Successfully exported certificate to .\TeamsCert.pem and .\TeamsPriv.key'
                }
                Catch {
                    Get-JSONErrorStream -JSONResponse $_
                }
            }
            Else {
                Return $JSON.data
            }                
        }
        Catch {
            Write-Error "TenantName: $TenantName - No MS Teams information found, or insufficient permissions."
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



Function Get-NectarAzureADSubscription {
    <#
        .SYNOPSIS
        Return data about the Azure AD subscription for a given tenant.
         
        .DESCRIPTION
        Return data about the Azure AD subscription for a given tenant. Requires a global admin account. Not available to tenant-level admins.
 
        .EXAMPLE
        Get-NectarAzureADSubscription
 
        .NOTES
        Version 1.0
    #>

    
    [Alias("gnads")]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [switch]$ExportCertificate,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        Try {
            If (!$TenantName) { $TenantName = Get-NectarDefaultTenantName }

            $JSON = Invoke-RestMethod -Method GET -Credential $Global:NectarCred -uri "https://$Global:NectarCloud/aapi/clouddatasources/configuration/azuread/subscription?tenant=$TenantName"
            
            If ($ExportCertificate) {
                Try {
                    $JSON.data.msClientCertificateDto.certificate | Out-File TeamsCert.pem
                    $JSON.data.msClientCertificateDto.privateKey | Out-File TeamsPriv.key
                    Write-Host 'Successfully exported certificate to .\TeamsCert.pem and .\TeamsPriv.key'
                }
                Catch {
                    Get-JSONErrorStream -JSONResponse $_
                }
            }
            Else {
                Return $JSON.data
            }                
        }
        Catch {
            Write-Error "TenantName: $TenantName - No Azure AD information found, or insufficient permissions."
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



Function Get-NectarMSTeamsConfig {
    <#
        .SYNOPSIS
        Return information about an existing MS Teams call data integration with Nectar 10
         
        .DESCRIPTION
        Return information about an existing MS Teams call data integration with Nectar 10. Requires a global admin account. Not available to tenant-level admins.
         
        .PARAMETER TenantName
        The name of the Nectar 10 tenant. Used in multi-tenant configurations.
         
        .EXAMPLE
        Get-NectarMSTeamsConfig
 
        .NOTES
        Version 1.0
    #>

    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }

        # Get the existing MS Teams configurations
        $TeamsURI = "https://$Global:NectarCloud/aapi/clouddatasources/configuration/msteams/cdr?tenant=$TenantName"
        $TeamsBody = (Invoke-RestMethod -Method GET -Credential $Global:NectarCred -uri $TeamsURI).data

        Return $TeamsBody
    }
}


Function Get-NectarMSAzureConfig {
    <#
        .SYNOPSIS
        Return information about an existing MS Azure AD integration with Nectar 10
         
        .DESCRIPTION
        Return information about an existing MS Azure AD integration with Nectar 10. Requires a global admin account. Not available to tenant-level admins.
         
        .PARAMETER TenantName
        The name of the Nectar 10 tenant. Used in multi-tenant configurations.
         
        .EXAMPLE
        Get-NectarMSAzureConfig
 
        .NOTES
        Version 1.0
    #>

    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }

        # Get the existing MS Teams configurations
        $AzureURI = "https://$Global:NectarCloud/aapi/clouddatasources/configuration/azuread?tenant=$TenantName"
        $AzureBody = (Invoke-RestMethod -Method GET -Credential $Global:NectarCred -uri $AzureURI).data

        Return $AzureBody
    }
}


Function Set-NectarMSTeamsConfig {
    <#
        .SYNOPSIS
        Modify an existing MS Teams call data integration with Nectar 10
         
        .DESCRIPTION
        Modify an existing MS Teams call data integration with Nectar 10. Requires a global admin account. Not available to tenant-level admins.
         
        .PARAMETER MSClientID
        The MS client ID for the application granted access to Azure AD.
         
        .PARAMETER MSClientSecret
        The MS client secret for the application granted access to Azure AD. Use either this or CertID, but not both.
         
        .PARAMETER CertID
        The Nectar 10 certificate ID. Use either this or MSClientSecret but not both. Obtain the cert ID by running Get-NectarMSTeamsCertificate.
         
        .PARAMETER MSTenantID
        The MS tenant ID for the O365 customer granted access to Azure AD.
                 
        .PARAMETER TenantName
        The name of the Nectar 10 tenant. Used in multi-tenant configurations.
         
        .EXAMPLE
        Set-NectarMSTeamsConfig -CertID
 
        .NOTES
        Version 1.0
    #>

    
    [cmdletbinding(SupportsShouldProcess, ConfirmImpact = 'High')]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$MSClientID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$MSClientSecret,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [int]$CertID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$MSTenantID
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }

        # Get the existing MS Teams configurations
        $TeamsURI = "https://$Global:NectarCloud/aapi/clouddatasources/configuration/msteams/cdr?tenant=$TenantName"
        $AzureURI = "https://$Global:NectarCloud/aapi/clouddatasources/configuration/azuread?tenant=$TenantName"
        
        $TeamsBody = (Invoke-RestMethod -Method GET -Credential $Global:NectarCred -uri $TeamsURI).data[0]
        $AzureBody = (Invoke-RestMethod -Method GET -Credential $Global:NectarCred -uri $AzureURI).data[0]
        
    
        # Build body for updating
        If (!$MSTenantID) { $MSTenantID = $TeamsBody.MSTenantID }
        If (!$MSClientID) { $MSClientID = $TeamsBody.msClientID }
        If (!$MSClientSecret -and !$CertID) { $MSClientSecret = $TeamsBody.msClientSecret; $CertID = $TeamsBody.msClientCertificateId }
        
        $TeamsUpdateBody = @{
            tenant = $TenantName
            cloudAgentName = 'cloudconnector_agent'
            displayName = $TeamsBody.displayName
            msTenantId = $MSTenantID
            msClientId = $MSClientID
            msClientSecret = $MSClientSecret
            msClientCertificateId = $CertID
            kafkaTopic = $TeamsBody.kafkaTopic
            sourceId = 1
        }
        
        $AzureUpdateBody = @{
            tenant = $TenantName
            cloudAgentName = 'cloudconnector_agent'
            displayName = $AzureBody.displayName
            msTenantId = $MSTenantID
            msClientId = $MSClientID
            msClientSecret = $MSClientSecret
            msClientCertificateId = $CertID
            kafkaTopic = $AzureBody.kafkaTopic
            sourceId = 1
            primaryAD = $AzureBody.isPrimary
        }
        
        If ($CertID -and !$MSClientSecret) {
            $TeamsUpdateBody.msClientSecret = $NULL
            $AzureUpdateBody.msClientSecret = $NULL
        }
        ElseIf ($MSClientSecret -and !$CertID) {
            $TeamsUpdateBody.msClientCertificateId = $NULL
            $AzureUpdateBody.msClientCertificateId = $NULL
        }
        
        $TeamsJSONBody = $TeamsUpdateBody | ConvertTo-Json
        $AzureJSONBody = $AzureUpdateBody | ConvertTo-Json
        
        $TeamsUpdateURI = "https://$Global:NectarCloud/aapi/clouddatasources/configuration/msteams/cdr/$($TeamsBody.id)"
        $AzureUpdateURI = "https://$Global:NectarCloud/aapi/clouddatasources/configuration/azuread/$($AzureBody.id)"
        
        Write-Verbose $TeamsUpdateURI
        Write-Verbose $TeamsJSONBody
        Write-Verbose $AzureUpdateURI
        Write-Verbose $AzureJSONBody
        
        If ($PSCmdlet.ShouldProcess(("Updating Nectar 10 MS Teams config on tenant {0}" -f $TenantName), ("Update Nectar 10 MS Teams config on tenant {0}?" -f $TenantName), 'Nectar 10 MS Teams Config Update')) {
            Try {
                $JSON = Invoke-RestMethod -Method PUT -Credential $Global:NectarCred -uri $TeamsUpdateURI -Body $TeamsJSONBody -ContentType 'application/json; charset=utf-8'
            }
            Catch {
                Get-JSONErrorStream -JSONResponse $_
            }
        
            Try {
                $JSON = Invoke-RestMethod -Method PUT -Credential $Global:NectarCred -uri $AzureUpdateURI -Body $AzureJSONBody -ContentType 'application/json; charset=utf-8'
            }
            Catch {
                Get-JSONErrorStream -JSONResponse $_
            }
        }
    }
}




Function New-NectarMSTeamsConfig {
    <#
        .SYNOPSIS
        Enables MS Teams call data and Azure AD integration with Nectar 10
         
        .DESCRIPTION
        Enables MS Teams call data and Azure AD integration with Nectar 10. Requires a global admin account. Not available to tenant-level admins.
         
        .PARAMETER MSClientID
        The MS client ID for the application granted access to Azure AD.
         
        .PARAMETER MSClientSecret
        The MS client secret for the application granted access to Azure AD. Use either this or CertID, but not both.
         
        .PARAMETER CertID
        The Nectar 10 certificate ID. Use either this or MSClientSecret but not both. Obtain the cert ID by running Get-NectarMSTeamsCertificate.
         
        .PARAMETER MSTenantID
        The MS tenant ID for the O365 customer granted access to Azure AD.
                 
        .PARAMETER TenantName
        The name of the Nectar 10 tenant. Used in multi-tenant configurations.
         
        .EXAMPLE
        New-NectarMSTeamsConfig
 
        .NOTES
        Version 1.0
    #>

    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$MSClientID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$MSClientSecret,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [int]$CertID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$MSTenantID
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
        
        $TeamsURI = "https://$Global:NectarCloud/aapi/clouddatasources/configuration/msteams/cdr"
        $AzureURI = "https://$Global:NectarCloud/aapi/clouddatasources/configuration/azuread"
        
        # Get the cloud agent name
        $CloudAgentURI = "https://$Global:NectarCloud/aapi/cloudagents"
        $CloudAgentName = (Invoke-RestMethod -Method GET -Credential $Global:NectarCred -uri $CloudAgentURI)[0].cloudAgentName
        
        # Check for existing Teams/Azure config
        # Will increment the sourceID if one already exists
        [int]$TeamsSourceID = ((Get-NectarMSTeamsConfig).sourceID | Measure-Object -Maximum).Maximum + 1
        [int]$AzureSourceID = ((Get-NectarMSAzureConfig).sourceID | Measure-Object -Maximum).Maximum + 1
        
        # Build the JSON body for creating the config
        $TeamsBody = @{
            tenant = $TenantName
            cloudAgentName = $CloudAgentName
            displayName = 'MS Teams'
            msTenantId = $MSTenantID
            msClientId = $MSClientID
            kafkaTopic = "$($TenantName)_msteams"
            sourceId = $TeamsSourceID
        }
        
        $AzureBody = @{
            tenant = $TenantName
            cloudAgentName = $CloudAgentName
            displayName = 'Azure AD'
            msTenantId = $MSTenantID
            msClientId = $MSClientID
            kafkaTopic = "$($TenantName)_azuread"
            sourceId = $AzureSourceID
            primaryAD = $TRUE
        }        

        If ($CertID) {
            $TeamsBody.Add('msClientCertificateId',$CertID)
            $AzureBody.Add('msClientCertificateId',$CertID)
        }
        Else {
            $TeamsBody.Add('msClientSecret',$MSClientSecret)
            $AzureBody.Add('msClientSecret',$MSClientSecret)            
        }
            
        $TeamsJSONBody = $TeamsBody | ConvertTo-Json
        $AzureJSONBody = $AzureBody | ConvertTo-Json
        
        Write-Verbose $TeamsURI
        Write-Verbose $TeamsJSONBody
        Write-Verbose $AzureURI
        Write-Verbose $AzureJSONBody
        
        Try {
            $JSON = Invoke-RestMethod -Method POST -Credential $Global:NectarCred -uri $TeamsURI -Body $TeamsJSONBody -ContentType 'application/json; charset=utf-8'
        }
        Catch {
            Get-JSONErrorStream -JSONResponse $_
        }
        
        Try {
            $JSON = Invoke-RestMethod -Method POST -Credential $Global:NectarCred -uri $AzureURI -Body $AzureJSONBody -ContentType 'application/json; charset=utf-8'
        }
        Catch {
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}




Function Get-NectarMSTeamsCertificate {
    <#
        .SYNOPSIS
        Returns information about a self-signed certificate for Azure app certificate-based authentication
         
        .DESCRIPTION
        MS Azure allows for either client secret or certificate-based authentication for Azure applications. This shows the certificate details for a given tenant.
        Requires a global admin account. Not available to tenant-level admins.
         
        .PARAMETER CertID
        The Nectar 10 certificate ID. Typically starts at 1 and increments from there
         
        .PARAMETER CertFilePath
        Exports the certificate to a .cer file at the listed path
         
        .EXAMPLE
        Get-NectarMSTeamsCertificate -CertID 1
 
        .NOTES
        Version 1.0
    #>

    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [int]$CertID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$CertFilePath
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        Try {
            $JSON = Invoke-RestMethod -Method GET -Credential $Global:NectarCred -uri "https://$Global:NectarCloud/aapi/client/certificate?id=$CertID"
            
            If ($CertFilePath) {
                $JSON.data.certificate | Out-File -FilePath $CertFilePath
                Write-Host "Certificate file written to $CertFilePath"
            }
            
            Return $JSON.data
        }
        Catch {
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



Function New-NectarMSTeamsCertificate {
    <#
        .SYNOPSIS
        Creates a self-signed certificate for Azure app certificate-based authentication
         
        .DESCRIPTION
        MS Azure allows for either client secret or certificate-based authentication for Azure applications. This command will create a self-signed certificate to use for this purpose.
        It will create the private key, associate it with the Nectar 10 tenant and present the public portion to be added to the MS Azure app. Requires a global admin account. Not available to tenant-level admins.
         
        .PARAMETER CertDisplayName
        The display name to assign to the certificate
         
        .PARAMETER ValidityPeriod
        The number of days the certificate should be valid for. Defaults to 365 days (1 year)
         
        .EXAMPLE
        New-NectarMSTeamsCertificate -CertDisplayName 'CompanyName Certificate for Nectar 10' -ValidityPeriod 730
 
        .NOTES
        Version 1.0
    #>

    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$CertDisplayName = 'Nectar 10 Certificate for Azure App',
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [int]$ValidityPeriod = 365
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        Try {
            If (!$CertDisplayName) { }
            $JSON = Invoke-RestMethod -Method POST -Credential $Global:NectarCred -uri "https://$Global:NectarCloud/aapi/client/certificate?name=$CertDisplayName&validityDays=$ValidityPeriod"
            Return $JSON.data
        }
        Catch {
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}


Function Remove-NectarMSTeamsCertificate {
    <#
        .SYNOPSIS
        Removes a previously created certificate used for MS Teams Azure app authentication
         
        .DESCRIPTION
        MS Azure allows for either client secret or certificate-based authentication for Azure applications. This command will delete a previously created self-signed certificate.
        Requires a global admin account. Not available to tenant-level admins.
         
        .PARAMETER CertID
        The Nectar 10 certificate ID. Typically starts at 1 and increments from there
         
        .EXAMPLE
        Remove-NectarMSTeamsCertificate -CertID 1
 
        .NOTES
        Version 1.0
    #>

    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [int]$CertID
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        Try {
            $JSON = Invoke-RestMethod -Method DELETE -Credential $Global:NectarCred -uri "https://$Global:NectarCloud/aapi/client/certificate?id=$CertID"
        }
        Catch {
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}




Function Set-NectarMSTeamsUserMonitoringLimit {
    <#
        .SYNOPSIS
        Configure a limit to the number of users that are monitored in Nectar 10
         
        .DESCRIPTION
        Configure a limit to the number of users that are monitored in Nectar 10. Uses membership in a specified Azure AD group to determine the users we pull call records.
        Requires a global admin account. Not available to tenant-level admins.
         
        .PARAMETER MaxUsers
        The maximum number of users that will be monitored. Any group members above MaxUsers will not be monitored.
 
        .PARAMETER GroupName
        The display name of the group that contains the users who will be monitored. If the group contains spaces, enclose the name in quotes
         
        .PARAMETER TenantName
        The name of the Nectar 10 tenant. Used in multi-tenant configurations.
         
        .EXAMPLE
        Set-NectarMSTeamsUserMonitoringLimit -MaxUsers 1000 -GroupName N10Users -TenantName contoso
        Sets the maximum number of monitored users on the Contoso tenant to 1000 and uses the members of the N10Users group as the list of users to be monitored
 
        .NOTES
        Version 1.0
    #>

    
    [Alias("sntuml")]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [ValidateRange(1,999999)]
        [int]$MaxUsers,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [string]$GroupName,
        [Parameter(Mandatory=$False)]
        [bool]$Enabled = $True,    
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        # Use globally set tenant name, if one was set and not explicitly included in the command
        If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }

        If ($Enabled) {
            # Validate the group exists in Azure AD
            $AuthToken = Get-NectarMSTeamsSubscription -TenantName $TenantName | Get-MSGraphAccessToken
            
            $Headers = @{
                Authorization = "Bearer $AuthToken"
            }
        
            $URI = "https://graph.microsoft.com/v1.0/groups?`$filter=displayName eq '$GroupName'"
            Write-Verbose $URI
            
            Try {
                $GroupData = Invoke-RestMethod -Method GET -uri $URI -Headers $Headers
                
                If ($GroupData.Value.Count -eq 1) {
                    Write-Host "Found Azure AD group " -NoNewLine
                    Write-Host $GroupName -ForegroundColor Green
                    $CountURI = "https://graph.microsoft.com/v1.0/groups/$($GroupData.Value.id)/transitivemembers/microsoft.graph.user/$count"
                    
                    # Add header
                    $Headers.Add('ConsistencyLevel', 'eventual')
                    Write-Verbose $CountURI
                    $MemberCount = Invoke-RestMethod -Method GET -uri $CountURI -Headers $Headers
                    $TotalCount = $MemberCount.value.count
                    
                    While ($MemberCount.'@odata.nextLink') {
                        $NextURI = $MemberCount.'@odata.nextLink'
                        $MemberCount = Invoke-RestMethod -Method GET -uri $NextURI -Headers $Headers
                        $TotalCount += $MemberCount.value.count
                    }
                    
                    Write-Host "Group has $TotalCount members"
                }
                Else {
                    Write-Host "No group called '$GroupName' could be found in Azure AD" -ForegroundColor Red
                    Break
                }
            }
            Catch {
                Write-Host "There was an error while attempting to locate the group $GroupName in Azure AD" -ForegroundColor Red
                Break
            }
        }

        # Get the MS Teams Config ID for constructing the UrlEncode
        $MSTeamsConfigID = (Get-NectarMSTeamsSubscription).msTeamsConfigId
        
        $URI = "https://$Global:NectarCloud/aapi/clouddatasources/configuration/msteams/cdr/partialmonitoring?id=$MSTeamsConfigID"
        
        Write-Verbose $URI
        
        $Body = @{
            tenant = $TenantName
            enabled = $Enabled
            azureAdGroup = $GroupName
            maxUsers = $MaxUsers
        }
        
        $JSONBody = $Body | ConvertTo-Json
        
        Write-Verbose $JSONBody
        
        Try {
            $JSON = Invoke-RestMethod -Method PUT -Credential $Global:NectarCred -uri $URI -Body $JSONBody -ContentType 'application/json; charset=utf-8'
            If ($Enabled) {
                Write-Host "Successfully enabled Teams partial user monitoring on " -NoNewLine
                Write-Host $TenantName -NoNewLine -ForegroundColor Green 
                Write-Host " for up to " -NoNewLine
                Write-Host $MaxUsers -NoNewLine -ForegroundColor Green
                Write-Host " users using group named " -NoNewLine
                Write-Host $GroupName -NoNewLine -ForegroundColor Green
            }
            Else {
                Write-Host "Successfully disabled Teams partial user monitoring on " -NoNewLine
                Write-Host $TenantName -ForegroundColor Green
            }
        }
        Catch {
            Write-Error "Error while setting parameters"
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



Function Get-MSTeamsCallRecord {
    <#
        .SYNOPSIS
        Return a specific call record details directly from MS Azure tenant.
         
        .DESCRIPTION
        Return a specific call record details directly from MS Azure tenant.
         
        .PARAMETER CallRecordID
        The MS call record ID
         
        .PARAMETER TenantName
        The name of the Nectar 10 tenant. Used in multi-tenant configurations.
         
        .PARAMETER AuthToken
        The authorization token used for this request. Normally obtained via Get-MSGraphAccessToken
 
        .EXAMPLE
        Get-MSTeamsCallRecord -CallRecordID ed672235-5417-40ce-8425-12b8b702a505 -AuthToken $AuthToken
        Returns the given MS Teams call record using a previously-obtained authtoken
         
        .EXAMPLE
        Get-MSTeamsCallRecord -CallRecordID ed672235-5417-40ce-8425-12b8b702a505 -AuthToken $AuthToken -TextOutput | Out-File Call.json
        Returns the given MS Teams call record using a previously-obtained authtoken and saves the results to a .JSON file.
 
        .NOTES
        Version 1.3
    #>

    
    [Alias("gmtcr")]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [Alias("calldCallRecordId")]
        [string]$CallRecordID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$AuthToken,
        [Parameter(Mandatory=$False)]
        [switch]$TextOutput,
        [Parameter(Mandatory=$False)]
        [switch]$BetaCallRecord
    )
    
    Begin {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }    
            
            If (!$AuthToken) { $AuthToken = Get-NectarMSTeamsSubscription -TenantName $TenantName | Where {$_.SubscriptionStatus -eq 'ACTIVE'} | Get-MSGraphAccessToken }    
        }
        Catch {
            Write-Error "Could not obtain authorization token."
            Get-JSONErrorStream -JSONResponse $_        
        }
    }
    Process {
        Try {
            $Headers = @{
                Authorization = "Bearer $AuthToken"
            }
            
            # Change case to lower
            $CallRecordID = $CallRecordID.ToLower()

            If ($BetaCallRecord) {
                $URI = "https://graph.microsoft.com/beta/communications/callRecords/$CallRecordID/?`$expand=sessions(`$expand=segments)"
            }
            Else {
                $URI = "https://graph.microsoft.com/v1.0/communications/callRecords/$CallRecordID/?`$expand=sessions(`$expand=segments)"
            }
            
            Write-Verbose $URI
            
            $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Headers
            
            If ($JSON.'sessions@odata.nextLink') {
                $NextURI = $JSON.'sessions@odata.nextLink'
                Do  {
                    Write-Verbose "Getting next session page: $NextURI"
                    $NextJSON = Invoke-RestMethod -Method GET -uri $NextURI -Headers $Headers
                    $JSON.sessions += $NextJSON.value
                    Write-Verbose "Session count: $($NextJSON.value.count)"
                    $NextURI = $NextJSON.'@odata.nextLink'
                } Until (!$NextURI)
            }
            
            If ($TextOutput) {
                $JSON = $JSON | ConvertTo-Json -Depth 8
            }

            Return $JSON

        }
        Catch {
            Write-Error "Could not get call record."
            Write-Error $_
        }
    }
}



Function Get-MSTeamsDRCalls {
    <#
        .SYNOPSIS
        Return a list of MS Teams Direct Routing calls
         
        .DESCRIPTION
        Return a list of MS Teams Direct Routing calls
         
        .PARAMETER FromDateTime
        Start of time range to query. UTC, inclusive. Time range is based on the call start time. Defaults to today
 
        .PARAMETER ToDateTime
        End of time range to query. UTC, inclusive. Defaults to today
         
        .PARAMETER TenantName
        The name of the Nectar 10 tenant. Used in multi-tenant configurations.
 
        .PARAMETER MSClientID
        The MS client ID for the application granted access to Azure AD.
         
        .PARAMETER MSClientSecret
        The MS client secret for the application granted access to Azure AD.
         
        .PARAMETER MSTenantID
        The MS tenant ID for the O365 customer granted access to Azure AD.
 
        .PARAMETER AuthToken
        The authorization token used for this request. Normally obtained via Get-MSGraphAccessToken
         
        .PARAMETER ResultSize
        The number of results to return. Defaults to 1000.
         
        .EXAMPLE
        Get-MSTeamsDRCalls -MSClientID 41a228ad-db6c-4e4e-4184-6d8a1175a35f -MSClientSecret 43Rk5Xl3K349w-pFf0i_Rt45Qd~ArqkE32. -MSTenantID 17e1e614-8119-48ab-8ba1-6ff1d94a6930
        Returns all Teams DR calls for the specified tenant/clientID/secret combination
         
        .EXAMPLE
        Get-MSTeamsDRCalls -AuthToken $AuthToken
        Returns all Teams DR calls using a previously obtained authtoken
         
        .NOTES
        Version 1.0
    #>


    
    Param (
        [Parameter(Mandatory=$False)]
        [string]$FromDateTime = (Get-Date -Format 'yyyy-MM-dd'),
        [Parameter(Mandatory=$False)]
        [string]$ToDateTime = ((Get-Date).AddDays(+1).ToString('yyyy-MM-dd')),        
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$MSClientID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$MSClientSecret,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$MSTenantID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$AuthToken,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,500000)]
        [int]$ResultSize
    )

    Process {
        $Body = @{}
        $Params = @{}
        
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }    
            
            If ($MSTenantID) { 
                $AuthToken = Get-MSGraphAccessToken -MSClientID $MSClientID -MSClientSecret $MSClientSecret -MSTenantID $MSTenantID
            }
            ElseIf (!$AuthToken) {
                $AuthToken = Get-NectarMSTeamsSubscription -TenantName $TenantName | Get-MSGraphAccessToken
            }
        }
        Catch {
            Write-Error "Could not obtain authorization token."
            Get-JSONErrorStream -JSONResponse $_        
        }
        
        If ($AuthToken) {
            $Headers = @{
                Authorization = "Bearer $AuthToken"
            }

            $URI = "https://graph.microsoft.com/beta/communications/callRecords/getDirectRoutingCalls(fromDateTime=$FromDateTime,toDateTime=$ToDateTime)"
            
            Write-Verbose $URI
            
            $Params = @{}
            If ($AuthToken) { $Params.Add('AuthToken',$AuthToken) }
            If ($TenantName) { $Params.Add('Tenant',$TenantName) }
            
            $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Headers -Body $Body
            $JSON.value
            
            $PageSize = $JSON.value.count
            
            While (($JSON.'@odata.nextLink') -And ($PageSize -lt $ResultSize)) {
                $NextURI = $JSON.'@odata.nextLink'
                $JSON = Invoke-RestMethod -Method GET -uri $NextURI -Headers $Headers
                $JSON.value
                $PageSize += $JSON.value.count
            }
            Clear-Variable -Name AuthToken
        }
    }
}


Function Get-MSAzureUsers {
    <#
        .SYNOPSIS
        Return a list of users from Azure AD.
         
        .DESCRIPTION
        Return a list of users from Azure AD.
         
        .PARAMETER Properties
        A comma-delimited list of properties to return in the results
        Format as per https://docs.microsoft.com/en-us/graph/query-parameters?view=graph-rest-1.0#select-parameter
        Available properties can be found at https://docs.microsoft.com/en-us/graph/api/resources/user?view=graph-rest-1.0#properties
         
        .PARAMETER Filter
        Add filter parameters as per https://docs.microsoft.com/en-us/graph/query-parameters?context=graph%2Fapi%2F1.0&view=graph-rest-1.0#filter-parameter
        Available properties can be found at https://docs.microsoft.com/en-us/graph/api/resources/user?view=graph-rest-1.0#properties
         
        .PARAMETER TenantName
        The name of the Nectar 10 tenant. Used in multi-tenant configurations.
 
        .PARAMETER HideProgressBar
        Don't show the progress bar. Cleans up logs when running on Docker.
         
        .PARAMETER AuthToken
        The authorization token used for this request. Normally obtained via Get-MSGraphAccessToken
         
        .PARAMETER TotalCount
        Only return a total count of objects
         
        .PARAMETER ResultSize
        The number of results to return. Defaults to 1000.
         
        .EXAMPLE
        Get-MSAzureUsers
        Returns all MS Azure user accounts
         
        .EXAMPLE
        Get-MSAzureUsers -Properties 'id,userPrincipalName' -AuthToken $AuthToken
        Returns a list of Azure users' ID and userPrincipalNames using a previously-obtained authtoken
         
        .EXAMPLE
        Get-MSAzureUsers -Filter "startswith(displayName,'Meeting Room')"
        Returns a list of Azure users whose display names start with 'Meeting Room'
 
        .NOTES
        Version 1.3
    #>


    
    Param (
        [Parameter(Mandatory=$False)]
        [string]$Properties,
        [Parameter(Mandatory=$False)]
        [string]$Filter,        
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [switch]$TotalCount,
        [Parameter(Mandatory=$False)]
        [switch]$HideProgressBar,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$AuthToken,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,500000)]
        [int]$ResultSize,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,99999)]
        [int]$ProgressUpdateFreq = 1    
    )

    Process {
        $Body = @{}
        $Params = @{}
        
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }    
            
            If (!$AuthToken) { $AuthToken = Get-NectarMSTeamsSubscription -TenantName $TenantName | Get-MSGraphAccessToken }
        }
        Catch {
            Write-Error "Could not obtain authorization token."
            Get-JSONErrorStream -JSONResponse $_        
        }
        
        If ($AuthToken) {
            Try {
                $Headers = @{
                    Authorization = "Bearer $AuthToken"
                }

                If ($Properties) { 
                    $Body.Add('$select',$Properties)
                    $Params.Add('Properties',$Properties)
                }

                If ($Filter) { 
                    $Body.Add('$filter',$Filter)
                    $Params.Add('Filter',$Filter)
                }

                If ($TotalCount) {
                    $Body.Add('ConsistencyLevel','eventual')
                    
                    $URI = 'https://graph.microsoft.com/v1.0/users/$count'
                    $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Headers -Body $Body
                    
                    $UserCount = New-Object PsObject
                    $UserCount | Add-Member -NotePropertyName 'UserCount' -NotePropertyValue $JSON
                
                    If ($TenantName) { $UserCount | Add-Member -NotePropertyName 'TenantName' -NotePropertyValue $TenantName }
                    Clear-Variable -Name AuthToken
                    Return $UserCount
                }
                Else {
                    $URI = "https://graph.microsoft.com/v1.0/users"
                    
                    Write-Verbose $URI
                    
                    $Params = @{}
                    If ($AuthToken) { $Params.Add('AuthToken',$AuthToken) }
                    If ($TenantName) { $Params.Add('Tenant',$TenantName) }
                    
                    $TotalUsers = Get-MSAzureUsers @Params -TotalCount
                    
                    $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Headers -Body $Body
                    $JSON.value
                    
                    $PageSize = $JSON.value.count
                    
                    $Message = "Getting Azure AD users for tenant $TenantName." 
                    
                    If ($HideProgressBar) { Write-Host $Message }
                    
                    $UserCount = 0
                    $LastCount = 0
                    $Stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
                    
                    While (($JSON.'@odata.nextLink') -And ($PageSize -lt $ResultSize)) {
                        $NextURI = $JSON.'@odata.nextLink'
                        $JSON = Invoke-RestMethod -Method GET -uri $NextURI -Headers $Headers
                        $JSON.value
                        $PageSize += $JSON.value.count
                        
                        If ($Stopwatch.Elapsed.TotalSeconds -ge $ProgressUpdateFreq) {
                            $Percentage = ($PageSize / $TotalUsers.UserCount) * 100
                                                    
                            If ($HideProgressBar) {
                                $Percentage = [math]::Round($Percentage,1)
                                Write-Host "Retrieving $Percentage`% of $($TotalUsers.UserCount) users on tenant $TenantName..."
                            }
                            Else { 
                                Write-Progress -Activity $Message -PercentComplete $Percentage -Status 'Retrieving...' 
                            }
                        
                            $Stopwatch.Reset()
                            $Stopwatch.Start()
                        }
                    }
                }
                Clear-Variable -Name AuthToken
            }
            Catch {
                Write-Error "Could not get user data."
                Write-Error $_
                Clear-Variable -Name AuthToken
            }
        }
    }
}



Function Get-MSAzureUserGroupMembership {
    <#
        .SYNOPSIS
        Return the groups that a user is a member of from Azure AD.
         
        .DESCRIPTION
        Return the groups that a user is a member of from Azure AD.
         
        .PARAMETER ID
        The Azure AD GUID of the user
         
        .PARAMETER Transitive
        Show groups where the user is a member of a group that is a member of another group
         
        .PARAMETER Properties
        A comma-delimited list of properties to return in the results
        Format as per https://docs.microsoft.com/en-us/graph/query-parameters?view=graph-rest-1.0#select-parameter
        Available properties can be found at https://docs.microsoft.com/en-us/graph/api/resources/group?view=graph-rest-1.0#properties
         
        .PARAMETER Filter
        Add filter parameters as per https://docs.microsoft.com/en-us/graph/query-parameters?context=graph%2Fapi%2F1.0&view=graph-rest-1.0#filter-parameter
        Available properties can be found at https://docs.microsoft.com/en-us/graph/api/resources/group?view=graph-rest-1.0#properties
         
        .PARAMETER TenantName
        The name of the Nectar 10 tenant. Used in multi-tenant configurations.
 
        .PARAMETER AuthToken
        The authorization token used for this request. Normally obtained via Get-MSGraphAccessToken
         
        .PARAMETER TotalCount
        Only return a total count of objects
         
        .PARAMETER ResultSize
        The number of results to return. Defaults to 1000.
         
        .EXAMPLE
        Get-MSAzureUserGroupMembership -ID abcdefab-1234-1234-1234-abcdabcdabcd
        Returns the groups that the selected user is a member of
         
        .EXAMPLE
        Get-MSAzureUsers -Properties 'id,userPrincipalName' -AuthToken $AuthToken
        Returns a list of Azure users' ID and userPrincipalNames using a previously-obtained authtoken
 
        .NOTES
        Version 1.0
    #>


    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [string]$ID,    
        [Parameter(Mandatory=$False)]
        [switch]$Transitive,
        [string]$Properties,
        [Parameter(Mandatory=$False)]
        [string]$Filter,        
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [switch]$TotalCount,
        [Parameter(Mandatory=$False)]
        [switch]$HideProgressBar,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$AuthToken,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,500000)]
        [int]$ResultSize = 1000,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,99999)]
        [int]$ProgressUpdateFreq = 1    
    )
    
    Begin {
        If ($Transitive) {
            $MemberOfScope = 'transitiveMemberOf'
        }
        Else {
            $MemberOfScope = 'MemberOf'
        }
    }
    Process {
        $Body = @{}
        $Params = @{}
        
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }    
            
            If (!$AuthToken) { $AuthToken = Get-NectarMSTeamsSubscription -TenantName $TenantName | Get-MSGraphAccessToken }
        }
        Catch {
            Write-Error "Could not obtain authorization token."
            Get-JSONErrorStream -JSONResponse $_        
        }
        
        If ($AuthToken) {
            Try {
                $Headers = @{
                    Authorization = "Bearer $AuthToken"
                }

                If ($Properties) { 
                    $Body.Add('$select',$Properties)
                    $Params.Add('Properties',$Properties)
                }

                If ($Filter) { 
                    $Body.Add('$filter',$Filter)
                    $Params.Add('Filter',$Filter)
                }

                If ($TotalCount) {
                    $Body.Add('ConsistencyLevel','eventual')
                    
                    $URI = "https://graph.microsoft.com/v1.0/users/$ID/$MemberOfScope/`$count"
                    Write-Verbose $URI
                    $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Headers -Body $Body
                    
                    $GroupCount = New-Object PsObject
                    $GroupCount | Add-Member -NotePropertyName 'UserCount' -NotePropertyValue $JSON
                
                    If ($TenantName) { $GroupCount | Add-Member -NotePropertyName 'TenantName' -NotePropertyValue $TenantName }
                    Clear-Variable -Name AuthToken
                    Return $GroupCount
                }
                Else {
                    $URI = "https://graph.microsoft.com/v1.0/users/$ID/$MemberOfScope"
                    
                    Write-Verbose $URI
                    
                    $Params = @{}
                    If ($AuthToken) { $Params.Add('AuthToken',$AuthToken) }
                    If ($TenantName) { $Params.Add('Tenant',$TenantName) }
                    
                    $TotalUsers = Get-MSAzureUsers @Params -TotalCount
                    
                    $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Headers -Body $Body
                    $JSON.value
                    
                    $PageSize = $JSON.value.count
                    
                    While (($JSON.'@odata.nextLink') -And ($PageSize -lt $ResultSize)) {
                        $NextURI = $JSON.'@odata.nextLink'
                        $JSON = Invoke-RestMethod -Method GET -uri $NextURI -Headers $Headers
                        $JSON.value
                        $PageSize += $JSON.value.count
                    }
                }
                Clear-Variable -Name AuthToken
            }
            Catch {
                Write-Error "Could not get group membership data."
                Write-Error $_
                Clear-Variable -Name AuthToken
            }
        }
    }
}



Function Get-MSAzureGroup {
    <#
        .SYNOPSIS
        Return a list of groups from Azure AD.
         
        .DESCRIPTION
        Return a list of groups from Azure AD.
         
        .PARAMETER Properties
        A comma-delimited list of properties to return in the results
        Format as per https://docs.microsoft.com/en-us/graph/query-parameters?view=graph-rest-1.0#select-parameter
        Available properties can be found at https://docs.microsoft.com/en-us/graph/api/resources/group?view=graph-rest-1.0#properties
         
        .PARAMETER Filter
        Add filter parameters as per https://docs.microsoft.com/en-us/graph/query-parameters?context=graph%2Fapi%2F1.0&view=graph-rest-1.0#filter-parameter
        Available properties can be found at https://docs.microsoft.com/en-us/graph/api/resources/group?view=graph-rest-1.0#properties
         
        .PARAMETER TenantName
        The name of the Nectar 10 tenant. Used in multi-tenant configurations.
 
        .PARAMETER HideProgressBar
        Don't show the progress bar. Cleans up logs when running on Docker.
         
        .PARAMETER AuthToken
        The authorization token used for this request. Normally obtained via Get-MSGraphAccessToken
         
        .PARAMETER TotalCount
        Only return a total count of objects
         
        .PARAMETER ResultSize
        The number of results to return. Defaults to 1000.
         
        .EXAMPLE
        Get-MSAzureGroup
        Returns all MS Azure groups
         
        .EXAMPLE
        Get-MSAzureGroup -Properties 'id,DisplayName' -AuthToken $AuthToken
        Returns a list of Azure group ID and display names using a previously-obtained authtoken
 
        .NOTES
        Version 1.0
    #>


    
    Param (
        [Parameter(Mandatory=$False)]
        [string]$Properties,
        [Parameter(Mandatory=$False)]
        [string]$Filter,        
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [switch]$TotalCount,
        [Parameter(Mandatory=$False)]
        [switch]$HideProgressBar,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$AuthToken,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,500000)]
        [int]$ResultSize,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,99999)]
        [int]$ProgressUpdateFreq = 1    
    )

    Process {
        $Body = @{}
        $Params = @{}
        
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }    
            
            If (!$AuthToken) { $AuthToken = Get-NectarMSTeamsSubscription -TenantName $TenantName | Get-MSGraphAccessToken }
        }
        Catch {
            Write-Error "Could not obtain authorization token."
            Get-JSONErrorStream -JSONResponse $_        
        }
        
        If ($AuthToken) {
            Try {
                $Headers = @{
                    Authorization = "Bearer $AuthToken"
                }

                If ($Properties) { 
                    $Body.Add('$select',$Properties)
                    $Params.Add('Properties',$Properties)
                }

                If ($Filter) { 
                    $Body.Add('$filter',$Filter)
                    $Params.Add('Filter',$Filter)
                }

                If ($TotalCount) {
                    $Body.Add('ConsistencyLevel','eventual')
                    
                    $URI = 'https://graph.microsoft.com/v1.0/groups/$count'
                    $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Headers -Body $Body
                    
                    $GroupCount = New-Object PsObject
                    $GroupCount | Add-Member -NotePropertyName 'GroupCount' -NotePropertyValue $JSON
                
                    If ($TenantName) { $GroupCount | Add-Member -NotePropertyName 'TenantName' -NotePropertyValue $TenantName }
                    Clear-Variable -Name AuthToken
                    Return $GroupCount
                }
                Else {
                    $URI = "https://graph.microsoft.com/v1.0/groups"
                    
                    Write-Verbose $URI
                    
                    $Params = @{}
                    If ($AuthToken) { $Params.Add('AuthToken',$AuthToken) }
                    If ($TenantName) { $Params.Add('Tenant',$TenantName) }
                    
                    $TotalGroups = Get-MSAzureGroup @Params -TotalCount
                    
                    $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Headers -Body $Body
                    $JSON.value
                    
                    $PageSize = $JSON.value.count
                    
                    $Message = "Getting Azure AD groups for tenant $TenantName." 
                    
                    If ($HideProgressBar) { Write-Host $Message }
                    
                    $GroupCount = 0
                    $LastCount = 0
                    $Stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
                    
                    While (($JSON.'@odata.nextLink') -And ($PageSize -lt $ResultSize)) {
                        $NextURI = $JSON.'@odata.nextLink'
                        $JSON = Invoke-RestMethod -Method GET -uri $NextURI -Headers $Headers
                        $JSON.value
                        $PageSize += $JSON.value.count
                        
                        If ($Stopwatch.Elapsed.TotalSeconds -ge $ProgressUpdateFreq) {
                            $Percentage = ($PageSize / $TotalGroups.GroupCount) * 100
                                                    
                            If ($HideProgressBar) {
                                $Percentage = [math]::Round($Percentage,1)
                                Write-Host "Retrieving $Percentage`% of $($TotalGroups.GroupCount) users on tenant $TenantName..."
                            }
                            Else { 
                                Write-Progress -Activity $Message -PercentComplete $Percentage -Status 'Retrieving...' 
                            }
                        
                            $Stopwatch.Reset()
                            $Stopwatch.Start()
                        }
                    }
                }
                Clear-Variable -Name AuthToken
            }
            Catch {
                Write-Error "Could not get group data."
                Write-Error $_
                Clear-Variable -Name AuthToken
            }
        }
    }
}



Function Get-MSAzureGroupMembers {
    <#
        .SYNOPSIS
        Return a list of group members from Azure AD.
         
        .DESCRIPTION
        Return a list of group members from Azure AD.
         
        .PARAMETER ID
        The GUID of the group to return membership info
     
        .PARAMETER Transitive
        Show members where the members are members of a group that is a member of another group
         
        .PARAMETER Properties
        A comma-delimited list of properties to return in the results
        Format as per https://docs.microsoft.com/en-us/graph/query-parameters?view=graph-rest-1.0#select-parameter
        Available properties can be found at https://docs.microsoft.com/en-us/graph/api/resources/group?view=graph-rest-1.0#properties
         
        .PARAMETER Filter
        Add filter parameters as per https://docs.microsoft.com/en-us/graph/query-parameters?context=graph%2Fapi%2F1.0&view=graph-rest-1.0#filter-parameter
        Available properties can be found at https://docs.microsoft.com/en-us/graph/api/resources/group?view=graph-rest-1.0#properties
         
        .PARAMETER TenantName
        The name of the Nectar 10 tenant. Used in multi-tenant configurations.
 
        .PARAMETER AuthToken
        The authorization token used for this request. Normally obtained via Get-MSGraphAccessToken
         
        .PARAMETER ResultSize
        The number of results to return. Defaults to 1000.
         
        .EXAMPLE
        Get-MSAzureGroup
        Returns all MS Azure user accounts
         
        .EXAMPLE
        Get-MSAzureGroup -Properties 'id,userPrincipalName' -AuthToken $AuthToken
        Returns a list of Azure users' ID and userPrincipalNames using a previously-obtained authtoken
 
        .NOTES
        Version 1.0
    #>


    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$ID,
        [Parameter(Mandatory=$False)]
        [switch]$Transitive,
        [Parameter(Mandatory=$False)]
        [string]$Properties,
        [Parameter(Mandatory=$False)]
        [string]$Filter,    
        [Parameter(Mandatory=$False)]
        [switch]$TotalCount,        
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$AuthToken,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,500000)]
        [int]$ResultSize = 1000
    )

    Begin {
        If ($Transitive) {
            $MemberScope = 'transitiveMembers'
        }
        Else {
            $MemberScope = 'members'
        }
    }
    Process {
        $Body = @{}
        $Params = @{}
        
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }    
            
            If (!$AuthToken) { $AuthToken = Get-NectarMSTeamsSubscription -TenantName $TenantName | Get-MSGraphAccessToken }
        }
        Catch {
            Write-Error "Could not obtain authorization token."
            Get-JSONErrorStream -JSONResponse $_        
        }
        
        If ($AuthToken) {
            Try {
                $Headers = @{
                    Authorization = "Bearer $AuthToken"
                }

                If ($Properties) { 
                    $Body.Add('$select',$Properties)
                    $Params.Add('Properties',$Properties)
                }

                If ($Filter) { 
                    $Body.Add('$filter',$Filter)
                    $Params.Add('Filter',$Filter)
                }

                If ($TotalCount) {
                    $Body.Add('ConsistencyLevel','eventual')
                    
                    $URI = "https://graph.microsoft.com/v1.0/groups/$ID/$MemberScope/`$count"
                    $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Headers -Body $Body
                    
                    $MemberCount = New-Object PsObject
                    $MemberCount | Add-Member -NotePropertyName 'MemberCount' -NotePropertyValue $JSON
                
                    If ($TenantName) { $MemberCount | Add-Member -NotePropertyName 'TenantName' -NotePropertyValue $TenantName }
                    Clear-Variable -Name AuthToken
                    Return $MemberCount
                }
                Else {
                    $URI = "https://graph.microsoft.com/v1.0/groups/$ID/$MemberScope"
                    
                    Write-Verbose $URI
                    
                    $Params = @{}
                    If ($AuthToken) { $Params.Add('AuthToken',$AuthToken) }
                    If ($TenantName) { $Params.Add('Tenant',$TenantName) }
                    
                    $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Headers -Body $Body
                    $JSON.value
                    
                    $PageSize = $JSON.value.count
                    
                    While (($JSON.'@odata.nextLink') -And ($PageSize -lt $ResultSize)) {
                        $NextURI = $JSON.'@odata.nextLink'
                        $JSON = Invoke-RestMethod -Method GET -uri $NextURI -Headers $Headers
                        $JSON.value
                        $PageSize += $JSON.value.count
                    }
                }
                Clear-Variable -Name AuthToken
            }
            Catch {
                Write-Error "Could not get group member data."
                Write-Error $_
                Clear-Variable -Name AuthToken
            }
        }
    }
}




Function Get-MSAzurePhoto {
    <#
        .SYNOPSIS
        Return users who have a photo in MS Azure.
         
        .DESCRIPTION
        Return users who have a photo in MS Azure. Useful for troubleshooting
         
        .PARAMETER TenantName
        The name of the Nectar 10 tenant. Used in multi-tenant configurations.
         
        .PARAMETER HideProgressBar
        Don't show the progress bar. Cleans up logs when running on Docker.
         
        .PARAMETER ProgressUpdateFreq
        How frequently to show progress updates, in seconds. Defaults to 1 second
         
        .PARAMETER UserList
        Use a previously obtained list of Azure user IDs. If this isn't specified, then a list will be downloaded using Get-MSAzureUsers
         
        .PARAMETER MSClientID
        The MS client ID for the application granted access to Azure AD.
         
        .PARAMETER MSClientSecret
        The MS client secret for the application granted access to Azure AD.
         
        .PARAMETER MSTenantID
        The MS tenant ID for the O365 customer granted access to Azure AD.
         
        .EXAMPLE
        Get-MSAzurePhoto -TenantName contoso
 
        .NOTES
        Version 1.1
    #>

    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [switch]$HideProgressBar,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,99999)]
        [int]$ProgressUpdateFreq = 1,
        [Parameter(Mandatory=$False)]
        $UserList,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$MSClientID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$MSClientSecret,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$MSTenantID
    )
    
    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }    
            
            If ($MSTenantID) { 
                $AuthToken = Get-MSGraphAccessToken -MSClientID $MSClientID -MSClientSecret $MSClientSecret -MSTenantID $MSTenantID
            }
            Else {
                $AuthToken = Get-NectarMSTeamsSubscription -TenantName $TenantName | Get-MSGraphAccessToken
            }
        }
        Catch {
            Write-Error "Could not obtain authorization token for tenant $TenantName."
            Get-JSONErrorStream -JSONResponse $_        
        }
        
        If ($AuthToken) {
            $Headers = @{
                Authorization = "Bearer $AuthToken"
            }
            
            $Params = @{
                TenantName = $TenantName
                Properties = 'id'
                Filter = 'accountEnabled eq true'
                AuthToken = $AuthToken
                ResultSize = 200000
            }
            
            If ($HideProgressBar) { $Params.Add('HideProgressBar', $HideProgressBar) }
            If ($ProgressUpdateFreq) { $Params.Add('ProgressUpdateFreq', $ProgressUpdateFreq) }
            
            If (!$UserList) { $UserList = Get-MSAzureUsers @Params }
            
            $TotalUsers = $UserList.Count

            $UserCount = 0
            $PhotoCount = 0
            $LastCount = 0
            $Stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
            
            ForEach ($User in $UserList) {
                $Retry = $FALSE
                [int]$RetryCount = 0
                
                Do {
                    # The AuthToken will expire after an hour or so. This Try/Catch block will get a new AuthToken when an error occurs
                    Try {
                        $URI = "https://graph.microsoft.com/v1.0/users/$($User.id)/photo/"
                        Write-Verbose $URI
                        $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Headers
                        $PhotoCount++
                        $Retry = $FALSE
                    }
                    Catch {
                        # If ($RetryCount -gt 1) {
                            # Write-Error "Could not get user data for tenant $TenantName."
                            # Write-Error $_
                            # $Retry = $FALSE
                        # }
                        # Else {
                            # If ($MSTenantID) {
                                # $AuthToken = Get-MSGraphAccessToken -MSClientID $MSClientID -MSClientSecret $MSClientSecret -MSTenantID $MSTenantID
                            # }
                            # Else {
                                # $AuthToken = Get-NectarMSTeamsSubscription -TenantName $TenantName | Get-MSGraphAccessToken
                            # }
                            # $Headers = @{
                                # Authorization = "Bearer $AuthToken"
                            # }
                            # $RetryCount++
                        # }
                    }
                } While ($Retry -eq $TRUE)
                
                If ($Stopwatch.Elapsed.TotalSeconds -ge $ProgressUpdateFreq) {
                    $Percentage = $UserCount / $TotalUsers
                    $Rate = ($UserCount - $LastCount) / $ProgressUpdateFreq
                    $LastCount = $UserCount
                    $Message = "Checking Azure photo for user {0} of {1} on tenant {2} at a rate of {3}/sec." -f $UserCount, $TotalUsers, $TenantName, $Rate
                    
                    If ($HideProgressBar) {
                        Write-Host $Message
                    }
                    Else {
                        Write-Progress -Activity $Message -PercentComplete ($Percentage * 100) -Status 'Calculating...' 
                    }
                    
                    $Stopwatch.Reset()
                    $Stopwatch.Start()
                }
                                
                $UserCount++
            }
            
            $AzurePhoto = New-Object PsObject
            $AzurePhoto | Add-Member -NotePropertyName 'PhotoCount' -NotePropertyValue $PhotoCount 

            If ($TenantName) { $AzurePhoto | Add-Member -NotePropertyName 'TenantName' -NotePropertyValue $TenantName }
            
            Clear-Variable -Name AuthToken

            Return $AzurePhoto
        }
    }
}




Function Get-MSTeamsServiceStatus {
    <#
        .SYNOPSIS
        Return the current MS Teams service status from Office 365
         
        .DESCRIPTION
        Return the current MS Teams service status from Office 365
         
        .PARAMETER CallRecordID
        The MS call record ID
         
        .PARAMETER TenantName
        The name of the Nectar 10 tenant. Used in multi-tenant configurations.
 
        .EXAMPLE
        Get-MSTeamsServiceStatus
 
        .NOTES
        Version 1.0
    #>

    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [switch]$TextOutput
    )
    
    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }    
            
            $TenantInfo = Get-NectarMSTeamsSubscription -TenantName $TenantName
            
            $TenantID = $TenantInfo.msTenantID
            $AuthToken = $TenantInfo | Get-MSOfficeAccessToken    
        }
        Catch {
            Write-Error "Could not obtain authorization token."
            Get-JSONErrorStream -JSONResponse $_        
        }
        
        If ($AuthToken) {
            Try {
                $Headers = @{
                    Authorization = "Bearer $AuthToken"
                }

                $URI = "https://manage.office.com/api/v1.0/$($TenantID)/ServiceComms/CurrentStatus?`$filter=Workload eq 'microsoftteams'"
                
                Write-Verbose $URI
                
                $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Headers
                
                If ($TextOutput) {
                    $JSON = $JSON | ConvertTo-Json -Depth 8
                }

                Clear-Variable -Name AuthToken

                Return $JSON.value

            }
            Catch {
                Write-Error $_.Exception.Response.StatusDescription
                Clear-Variable -Name AuthToken
            }
        }
    }
}



Function Get-MSTeamsServiceMessages {
    <#
        .SYNOPSIS
        Return any current service status messages regarding MS Teams cloud issues
         
        .DESCRIPTION
        Return any current service status messages regarding MS Teams cloud issues
         
        .PARAMETER TenantName
        The name of the Nectar 10 tenant. Used in multi-tenant configurations.
         
        .EXAMPLE
        Get-MSTeamsServiceMessages
 
        .NOTES
        Version 1.0
    #>

    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [switch]$Current,
        [Parameter(Mandatory=$False)]
        [switch]$TextOutput
    )

    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }    
            
            $TenantInfo = Get-NectarMSTeamsSubscription -TenantName $TenantName
            
            $TenantID = $TenantInfo.msTenantID
            $AuthToken = $TenantInfo | Get-MSOfficeAccessToken    
        }
        Catch {
            Write-Error "Could not obtain authorization token."
            Get-JSONErrorStream -JSONResponse $_        
        }

        If ($AuthToken) {    
            Try {
                $Headers = @{
                    Authorization = "Bearer $AuthToken"
                }
                
                $URI = "https://manage.office.com/api/v1.0/$($TenantID)/ServiceComms/Messages?`$filter=Workload eq 'microsoftteams'"
                
                If ($Current) { $URI = $URI + " and EndTime eq null" }
                
                Write-Verbose $URI
                
                $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Headers
                
                If ($TextOutput) {
                    $JSON = $JSON | ConvertTo-Json -Depth 8
                }

                Clear-Variable -Name AuthToken

                Return $JSON.value
            }
            Catch {
                Write-Error $_.Exception.Response.StatusDescription
                Clear-Variable -Name AuthToken
            }
        }
    }
}



Function Get-MSTeamsLicenseStatus {
    <#
        .SYNOPSIS
        Return the total number of SKUs for a company that have Teams in it
         
        .DESCRIPTION
        Return the total number of SKUs for a company that have Teams in it. Requires Organization.Read.All permission on graph.microsoft.com
         
        .PARAMETER TenantName
        The name of the Nectar 10 tenant. Used in multi-tenant configurations.
         
        .PARAMETER AuthToken
        The authorization token used for this request. Normally obtained via Get-MSGraphAccessToken
 
        .EXAMPLE
        Get-MSTeamsLicenseStatus
 
        .NOTES
        Version 1.0
    #>

    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$AuthToken
    )

    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }    
            
            If (!$AuthToken) { $AuthToken = Get-NectarMSTeamsSubscription -TenantName $TenantName | Get-MSGraphAccessToken }
        }
        Catch {
            Write-Error "Could not obtain authorization token for tenant $TenantName."
            Get-JSONErrorStream -JSONResponse $_        
        }
        
        If ($AuthToken) {
            Try {
                $Headers = @{
                    Authorization = "Bearer $AuthToken"
                }

                $URI = "https://graph.microsoft.com/v1.0/subscribedSkus"
                
                Write-Verbose $URI

                $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Headers

                $TotalSKUs = $JSON.value

                $LicenseCount = $TotalSKUs | Where {$_.servicePlans.ServicePlanName -eq 'TEAMS1'} | Select-Object skuPartNumber, ConsumedUnits
                
                If ($TotalOnly) {
                    $LicenseCount = $LicenseCount | Measure-Object -Property consumedUnits -Sum | Select-Object Sum
                }

                If ($TenantName) { $LicenseCount | Add-Member -NotePropertyName 'TenantName' -NotePropertyValue $TenantName }
                
                Clear-Variable -Name AuthToken
                
                Return $LicenseCount
            }
            Catch {
                Write-Error "Could not get license data for tenant $TenantName."
                Write-Error $_
                Clear-Variable -Name AuthToken
            }
        }
    }
}



Function Get-MSTeamsUserLicenseCount {
    <#
        .SYNOPSIS
        Return the total number of Teams licensed users for a given tenant.
         
        .DESCRIPTION
        Return the total number of Teams licensed users for a given tenant. Excludes disabled accounts.
         
        .PARAMETER TenantName
        The name of the Nectar 10 tenant. Used in multi-tenant configurations.
         
        .PARAMETER HideProgressBar
        Don't show the progress bar. Cleans up logs when running on Docker.
         
        .PARAMETER ProgressUpdateFreq
        How frequently to show progress updates, in seconds. Defaults to 1 second
         
        .PARAMETER UserList
        Use a previously obtained list of Azure user IDs. If this isn't specified, then a list will be downloaded using Get-MSAzureUsers
         
        .PARAMETER MSClientID
        The MS client ID for the application granted access to Azure AD.
         
        .PARAMETER MSClientSecret
        The MS client secret for the application granted access to Azure AD.
         
        .PARAMETER MSTenantID
        The MS tenant ID for the O365 customer granted access to Azure AD.
 
        .PARAMETER AuthToken
        The authorization token used for this request. Normally obtained via Get-MSGraphAccessToken
         
        .PARAMETER TestMode
        Sets the mode to run the tests under. Choose from Basic or Extended. Defaults to Basic mode
        Extended mode checks user O365 Service Plans for both Teams and Enterprise Voice functionality. Most accurate, but slower because it requires pulling
        down about 10x the data. The process ends up taking about twice as long as Basic mode.
        Basic mode looks at the assigned license SKUs, and also checks to see if TEAMS1 has been disabled. Faster because it requires about 10x less data than
        Extended mode, but can't obtain information about Enterprise Voice functionality. It's twice as fast as Extended mode, but only returns a total Teams count.
        It doesn't return the Enterprise Voice users.
 
        .EXAMPLE
        Get-MSTeamsUserLicenseCount -TenantName contoso
 
        .NOTES
        Version 1.2
    #>

    
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [switch]$HideProgressBar,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,99999)]
        [int]$ProgressUpdateFreq = 1,
        [Parameter(Mandatory=$False)]
        $UserList,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$MSClientID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$MSClientSecret,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$MSTenantID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$AuthToken,
        [Parameter(Mandatory=$False)]
        [ValidateSet('Basic','Extended', IgnoreCase=$True)]
        [string]$TestMode = 'Basic',
        [Parameter(Mandatory=$False)]
        [string]$ExportCSV
        
    )
    
    Begin {
        $TeamsSKUList = 
            '4b590615-0888-425a-a965-b3bf7789848d', # Microsoft 365 Education A3 for Faculty (M365EDU_A3_FACULTY)
            '7cfd9a2b-e110-4c39-bf20-c6a3f36a3121', # Microsoft 365 Education A3 for Students (M365EDU_A3_STUDENT)
            'e97c048c-37a4-45fb-ab50-922fbf07a370', # Microsoft 365 Education A5 for Faculty (M365EDU_A5_FACULTY)
            '46c119d4-0379-4a9d-85e4-97c66d3f909e', # Microsoft 365 Education A5 for Students (M365EDU_A5_STUDENT)
            '3b555118-da6a-4418-894f-7df1e2096870', # Microsoft 365 Business Basic (O365_BUSINESS_ESSENTIALS)
            'dab7782a-93b1-4074-8bb1-0e61318bea0b', # Microsoft 365 Business Basic (SMB_BUSINESS_ESSENTIALS)
            'f245ecc8-75af-4f8e-b61f-27d8114de5f3', # Microsoft 365 Business Standard (O365_BUSINESS_PREMIUM)
            'ac5cef5d-921b-4f97-9ef3-c99076e5470f', # Microsoft 365 Business Standard (SMB_BUSINESS_PREMIUM)
            'cbdc14ab-d96c-4c30-b9f4-6ada7cdc1d46', # Microsoft 365 Business Premium (SPB)
            '05e9a617-0261-4cee-bb44-138d3ef5d965', # Microsoft 365 E3 (SPB_E3)
            '06ebc4ee-1bb5-47dd-8120-11324bc54e06', # Microsoft 365 E5 (SPB_E5)
            '44575883-256e-4a79-9da4-ebe9acabe2b2', # Microsoft 365 F1 (M365_F1)
            '66b55226-6b4f-492c-910c-a3b7a3c9d993', # Microsoft 365 F3 (SPE_F1)
            'a4585165-0533-458a-97e3-c400570268c4', # Office 365 A5 for Faculty (ENTERPRISEPREMIUM_FACULTY)
            'ee656612-49fa-43e5-b67e-cb1fdf7699df', # Office 365 A5 for Students (ENTERPRISEPREMIUM_STUDENT)
            '18181a46-0d4e-45cd-891e-60aabd171b4e', # Office 365 E1 (STANDARDPACK)
            '6634e0ce-1a9f-428c-a498-f84ec7b8aa2e', # Office 365 E2 (STANDARDWOFFPACK)
            '6fd2c87f-b296-42f0-b197-1e91e994b900', # Office 365 E3 (ENTERPRISEPACK)
            '189a915c-fe4f-4ffa-bde4-85b9628d07a0', # Office 365 Developer (DEVELOPERPACK)
            '1392051d-0cb9-4b7a-88d5-621fee5e8711', # Office 365 E4 (ENTERPRISEWITHSCAL)
            'c7df2760-2c81-4ef7-b578-5b5392b571df', # Office 365 E5 (ENTERPRISEPREMIUM)
            '26d45bd9-adf1-46cd-a9e1-51e9a5524128', # Office 365 E5 without Audio Conferencing (ENTERPRISEPREMIUM_NOPSTNCONF)
            '4b585984-651b-448a-9e53-3b10f069cf7f', # Office 365 F1/3 (DESKLESSPACK)
            '6070a4c8-34c6-4937-8dfb-39bbc6397a60', # Meeting Room (MEETING_ROOM)
            '710779e8-3d4a-4c88-adb9-386c958d1fdf', # Teams Exploratory
            '7a551360-26c4-4f61-84e6-ef715673e083', # Dynamics 365 Remote Assist
            '50f60901-3181-4b75-8a2c-4c8e4c1d5a72', #
            '295a8eb0-f78d-45c7-8b5b-1eed5ed02dff'  # Common Area Phones
        
        $TeamsPlanID = '57ff2da0-773e-42df-b2af-ffb7a2317929'        # TEAMS1
        $TeamsPhoneSystemID = '4828c8ec-dc2e-4779-b502-87ac9ce28ab7' # MCOEV
        
        $TeamsCallingPlanList = 
            '4828c8ec-dc2e-4779-b502-87ac9ce28ab7', # MCOEV
            '4ed3ff63-69d7-4fb7-b984-5aec7f605ca8', # MCOPSTN1
            '5a10155d-f5c1-411a-a8ec-e99aae125390', # MCOPSTN2
            '54a152dc-90de-4996-93d2-bc47e670fc06'  # MCOPSTN5
        
        $UnmatchedSKUList = @()
        
        class UnmatchedSKU {
            [ValidateNotNullOrEmpty()][string]$SKUId
            [ValidateNotNullOrEmpty()][string]$UserId
        }
    }
    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }    
            
            If ($MSTenantID) { 
                $AuthToken = Get-MSGraphAccessToken -MSClientID $MSClientID -MSClientSecret $MSClientSecret -MSTenantID $MSTenantID
            }
            ElseIf (!$AuthToken) {
                $AuthToken = Get-NectarMSTeamsSubscription -TenantName $TenantName | Get-MSGraphAccessToken
            }
        }
        Catch {
            Write-Error "Could not obtain authorization token for tenant $TenantName."
            Get-JSONErrorStream -JSONResponse $_        
        }
        
        $TeamsCount = 0
        $TeamsEVCount = 0
        
        If ($AuthToken) {
            $Headers = @{
                Authorization = "Bearer $AuthToken"
            }
            
            If ($TestMode -eq 'Basic') {
                $Properties = 'id,assignedLicenses'
            }
            Else {
                $Properties = 'id,assignedPlans'
            }
            
            If ($ExportCSV) { $Properties += ',userPrincipalName,mail,displayName' }

            $Params = @{
                Properties = $Properties
                Filter = 'accountEnabled eq true'
                AuthToken = $AuthToken
                ResultSize = 200000
            }
            
            If ($TenantName) { $Params.Add('TenantName',$TenantName) }
            
            If ($HideProgressBar) { $Params.Add('HideProgressBar', $HideProgressBar) }
            If ($ProgressUpdateFreq) { $Params.Add('ProgressUpdateFreq', $ProgressUpdateFreq) }
            
            If (!$UserList) { 
                If ($TestMode -eq 'Basic') {
                    $UserList = Get-MSAzureUsers @Params | Where {$_.assignedLicenses -ne ''} 
                    $TotalUsers = $UserList.Count
                    $UserCount = 0
                    $LastCount = 0
                    $Stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
                    
                    ForEach ($User in $UserList) {
                        $Retry = $TRUE
                        [int]$RetryCount = 0
                        
                        $LicenseSKUs = $User.assignedLicenses
                        
                        # Find all SKUs that match the reference list
                        [string[]]$MatchingSKUs = (Compare-Object -ReferenceObject $TeamsSKUList -DifferenceObject $LicenseSKUs.skuId -IncludeEqual -ExcludeDifferent).InputObject
                        
                        # Check each SKU to make sure Teams isn't explicity disabled
                        :SKULoop ForEach ($MatchSKU in $MatchingSKUs) {
                            $TeamsSKUs = $LicenseSKUs | Where {$_.skuId -eq $MatchSKU}
                            ForEach ($SKU in $TeamsSKUs) {
                                If (!($SKU | Where {$_.disabledPlans -eq $TeamsPlanID})) {
                                    #Teams isn't disabled for this user. Increase the Teams user count and break out
                                    $TeamsCount++
                                    Break SKULoop
                                }
                            }
                        }
                        
                        If ($Stopwatch.Elapsed.TotalSeconds -ge $ProgressUpdateFreq) {
                            $Percentage = $UserCount / $TotalUsers
                            $Rate = ($UserCount - $LastCount) / $ProgressUpdateFreq
                            $LastCount = $UserCount
                            $Message = "Checking Teams license status for user {0} of {1} ({4} Teams users so far) on tenant {2} at a rate of {3}/sec." -f $UserCount, $TotalUsers, $TenantName, $Rate, $TeamsCount
                            
                            If ($HideProgressBar) {
                                Write-Host $Message
                            }
                            Else {
                                Write-Progress -Activity $Message -PercentComplete ($Percentage * 100) -Status 'Calculating...' 
                            }
                            
                            $Stopwatch.Reset()
                            $Stopwatch.Start()
                        }
                        $UserCount++
                    }
                }
                Else {
                    $UserList = Get-MSAzureUsers @Params | Where {$_.assignedPlans -ne ''}
                    $TeamsCount = ($UserList | Select-Object -ExpandProperty assignedPlans | Where {$_.capabilityStatus -eq 'Enabled' -and $_.servicePlanId -eq $TeamsPlanID}).Count
                    $TeamsEVCount = ($UserList | Select-Object -ExpandProperty assignedPlans | Where {$_.capabilityStatus -eq 'Enabled' -and $_.servicePlanId -eq $TeamsPhoneSystemID}).Count
                    
                    If ($ExportCSV) {
                        $UserList | Select-Object id,userPrincipalName,mail,displayName -ExpandProperty assignedPlans | Where {$_.capabilityStatus -eq 'Enabled' -and $_.servicePlanId -eq $TeamsPlanID} | Export-Csv $ExportCSV -NoTypeInformation -Force
                    }
                }
            }
            
            $TeamsLicense = New-Object PsObject
            $TeamsLicense | Add-Member -NotePropertyName 'TeamsCount' -NotePropertyValue $TeamsCount
            
            If ($TestMode -eq 'Extended') { $TeamsLicense | Add-Member -NotePropertyName 'EVCount' -NotePropertyValue $TeamsEVCount }
            
            If ($TenantName) { $TeamsLicense | Add-Member -NotePropertyName 'TenantName' -NotePropertyValue $TenantName }
            
            Clear-Variable -Name AuthToken

            Return $TeamsLicense
        }
    }
}






#################################################################################################################################################
# #
# Zoom Functions #
# #
#################################################################################################################################################

Function Get-NectarZoomAuthURL {
    <#
        .SYNOPSIS
        Returns the Nectar 10 Zoom authorization URL needed for connecting Nectar 10 to Zoom
         
        .DESCRIPTION
        Returns the Nectar 10 Zoom authorization URL needed for connecting Nectar 10 to Zoom
 
        .EXAMPLE
        Get-NectarZoomAuthURL
 
        .NOTES
        Version 1.0
    #>

    
    [Alias("gnzau")]
    [cmdletbinding()]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        Try {
            If (!$TenantName) { $TenantName = Get-NectarDefaultTenantName }

            $JSON = Invoke-RestMethod -Method GET -Credential $Global:NectarCred -uri "https://$Global:NectarCloud/aapi/zoom/oauth/url?tenant=$TenantName"
            Return $JSON.data
        }
        Catch {
            Write-Error 'No tenant Zoom data found, or insufficient permissions.'
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}




Function Get-ZoomAccessToken {
    <#
        .SYNOPSIS
        Get a Zoom JWT access token for a given Zoom tenant. Needed to run other Zoom API queries.
         
        .DESCRIPTION
        Get a Zoom access token for a given Zoom tenant. Needed to run other Zoom API queries. Generates a JSON Web Ticket (JWT)
 
        .EXAMPLE
        $AuthToken = Get-ZoomAccessToken -APIKey yourapikey -APISecret yourapisecret
 
        .NOTES
        Version 1.0
    #>

    
    Param(
        [Parameter(Mandatory = $False)]
        [ValidateSet('HS256', 'HS384', 'HS512')]
        $Algorithm = 'HS256',
        $type = $null,
        [Parameter(Mandatory = $True)]
        [string]$APIKey = $null,
        [int]$ValidforSeconds = 86400,
        [Parameter(Mandatory = $True)]
        $APISecret = $null
    )

    $exp = [int][double]::parse((Get-Date -Date $((Get-Date).addseconds($ValidforSeconds).ToUniversalTime()) -UFormat %s)) # Grab Unix Epoch Timestamp and add desired expiration.

    [hashtable]$header = @{alg = $Algorithm; typ = $type}
    [hashtable]$payload = @{iss = $APIKey; exp = $exp}

    $headerjson = $header | ConvertTo-Json -Compress
    $payloadjson = $payload | ConvertTo-Json -Compress
    
    $headerjsonbase64 = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($headerjson)).Split('=')[0].Replace('+', '-').Replace('/', '_')
    $payloadjsonbase64 = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($payloadjson)).Split('=')[0].Replace('+', '-').Replace('/', '_')

    $ToBeSigned = $headerjsonbase64 + "." + $payloadjsonbase64

    $SigningAlgorithm = switch ($Algorithm) {
        "HS256" {New-Object System.Security.Cryptography.HMACSHA256}
        "HS384" {New-Object System.Security.Cryptography.HMACSHA384}
        "HS512" {New-Object System.Security.Cryptography.HMACSHA512}
    }

    $SigningAlgorithm.Key = [System.Text.Encoding]::UTF8.GetBytes($APISecret)
    $Signature = [Convert]::ToBase64String($SigningAlgorithm.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($ToBeSigned))).Split('=')[0].Replace('+', '-').Replace('/', '_')
    
    $Token = "$headerjsonbase64.$payloadjsonbase64.$Signature"
    Return $Token
}



Function Get-ZoomMeeting {
    <#
        .SYNOPSIS
        Return a list of Zoom meetings.
         
        .DESCRIPTION
        Return a list of Zoom meetings.
         
        .PARAMETER MeetingID
        Return information about a specific meeting. Provide meeting ID or UUID. Don't use with From/To date range
         
        .PARAMETER Type
        Return live, past or past meetings with only one participant. Defaults to past
         
        .PARAMETER FromDateTime
        Start of date/time range to query. UTC, inclusive. Time range is based on the call start time. Defaults to yesterday.
 
        .PARAMETER ToDateTime
        End of date/time range to query. UTC, inclusive. Defaults to today
         
        .PARAMETER TenantName
        The name of the Nectar 10 tenant. Used in multi-tenant configurations.
 
        .PARAMETER APIKey
        The Zoom API key for the application granted access to Zoom.
         
        .PARAMETER APISecret
        The Zoom API secret for the application granted access to Zoom.
         
        .PARAMETER AuthToken
        The authorization token used for this request. Normally obtained via Get-ZoomAccessToken
         
        .PARAMETER PageSize
        The number of results to return per page. Defaults to 30.
         
        .EXAMPLE
        Get-ZoomMeetings -AuthToken $AuthToken
        Returns all past Zoom meetings from the previous 24 hours
         
        .NOTES
        Version 1.0
    #>


    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias("uuid")]
        [string]$MeetingID,    
        [Parameter(Mandatory = $False)]
        [ValidateSet('past', 'pastone', 'live')]
        $Type = 'past',
        [Parameter(Mandatory=$False)]
        [string]$FromDateTime = ((Get-Date).AddDays(-1).ToString('yyyy-MM-dd')),
        [Parameter(Mandatory=$False)]
        [string]$ToDateTime = (Get-Date -Format 'yyyy-MM-dd'),        
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$APIKey,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$APISecret,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$AuthToken,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,300)]
        [int]$PageSize = 30
    )

    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }    
            
            If ($APIKey) { 
                $AuthToken = Get-ZoomAccessToken -APIKey $APIKey -APISecret $APISecret
            }
            ElseIf (!$AuthToken) {
                $AuthToken = Get-NectarMSTeamsSubscription -TenantName $TenantName | Get-MSGraphAccessToken
            }
        }
        Catch {
            Write-Error "Could not obtain authorization token."
            Get-JSONErrorStream -JSONResponse $_        
        }
        
        If ($AuthToken) {
            $Headers = @{
                Authorization = "Bearer $AuthToken"
            }
            
            If ($MeetingID) {
                $URI = "https://api.zoom.us/v2/metrics/meetings/$MeetingID"
                
                $Body = @{
                    type = $Type
                }
                $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Headers -Body $Body
                $JSON
            }
            Else {
                $URI = "https://api.zoom.us/v2/metrics/meetings"
                
                $Body = @{
                    from = $FromDateTime
                    to = $ToDateTime
                    type = $Type
                    page_size = $PageSize
                }
                
                $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Headers -Body $Body
                $JSON.meetings
                
                # If there is more than one page, use next_page_token to iterate through the pages
                While ($JSON.next_page_token) {
                    $Body.next_page_token = $JSON.next_page_token
                    $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Headers -Body $Body
                    $JSON.meetings
                }
            }
            Clear-Variable -Name AuthToken
        }
    } 
}


Function Get-ZoomMeetingParticipants {
    <#
        .SYNOPSIS
        Return a list of Zoom meeting participants for a given meeting and their connection details.
         
        .DESCRIPTION
        Return a list of Zoom meeting participants for a given meeting and their connection details.
         
        .PARAMETER MeetingID
        The ID of the meeting. Provide meeting ID or UUID.
         
        .PARAMETER Type
        Return live, past or past meetings with only one participant. Defaults to past
         
        .PARAMETER TenantName
        The name of the Nectar 10 tenant. Used in multi-tenant configurations.
 
        .PARAMETER APIKey
        The Zoom API key for the application granted access to Zoom.
         
        .PARAMETER APISecret
        The Zoom API secret for the application granted access to Zoom.
         
        .PARAMETER AuthToken
        The authorization token used for this request. Normally obtained via Get-ZoomAccessToken
         
        .PARAMETER PageSize
        The number of results to return per page. Defaults to 30.
         
        .EXAMPLE
        Get-ZoomMeetingParticipants 928340928 -AuthToken $AuthToken
        Returns participant information from a specific meeting
         
        .NOTES
        Version 1.0
    #>


    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [Alias("uuid")]
        [string]$MeetingID,    
        [Parameter(Mandatory = $False)]
        [ValidateSet('past', 'pastone', 'live')]
        [string]$Type = 'past',
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$APIKey,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$APISecret,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$AuthToken,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,300)]
        [int]$PageSize = 30
    )

    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }    
            
            If ($APIKey) { 
                $AuthToken = Get-ZoomAccessToken -APIKey $APIKey -APISecret $APISecret
            }
            ElseIf (!$AuthToken) {
                $AuthToken = Get-NectarMSTeamsSubscription -TenantName $TenantName | Get-MSGraphAccessToken
            }
        }
        Catch {
            Write-Error "Could not obtain authorization token."
            Get-JSONErrorStream -JSONResponse $_        
        }
        
        If ($AuthToken) {
            $Headers = @{
                Authorization = "Bearer $AuthToken"
            }

            $URI = "https://api.zoom.us/v2/metrics/meetings/$MeetingID/participants"
            
            $Body = @{
                type = $Type
                page_size = $PageSize
            }
            
            $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Headers -Body $Body
            $JSON.participants
            
            # If there is more than one page, use next_page_token to iterate through the pages
            While ($JSON.next_page_token) {
                $Body.next_page_token = $JSON.next_page_token
                $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Headers -Body $Body
                $JSON.participants
            }
            Clear-Variable -Name AuthToken
        }
    } 
}



Function Get-ZoomMeetingParticipantQoS {
    <#
        .SYNOPSIS
        Return the participant QoS from a given meeting.
         
        .DESCRIPTION
        Return the participant QoS from a given meeting.
         
        .PARAMETER MeetingID
        Return information about a specific meeting.
         
        .PARAMETER ParticipantID
        Return information about a specific participant in a given meeting. Optional
         
        .PARAMETER Type
        Return live, past or past meetings with only one participant. Defaults to past
         
        .PARAMETER TenantName
        The name of the Nectar 10 tenant. Used in multi-tenant configurations.
 
        .PARAMETER APIKey
        The Zoom API key for the application granted access to Zoom.
         
        .PARAMETER APISecret
        The Zoom API secret for the application granted access to Zoom.
         
        .PARAMETER AuthToken
        The authorization token used for this request. Normally obtained via Get-ZoomAccessToken
         
        .PARAMETER PageSize
        The number of results to return per page. Defaults to 30.
         
        .EXAMPLE
        Get-ZoomMeetingParticipantQoS 928340928 -AuthToken $AuthToken
        Returns all past Zoom meetings from the previous 24 hours
         
        .NOTES
        Version 1.0
    #>


    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [Alias("uuid")]
        [string]$MeetingID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$ParticipantID,
        [Parameter(Mandatory = $False)]
        [ValidateSet('past', 'pastone', 'live')]
        [string]$Type = 'past',    
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$APIKey,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$APISecret,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$AuthToken,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,300)]
        [int]$PageSize = 30
    )

    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }    
            
            If ($APIKey) { 
                $AuthToken = Get-ZoomAccessToken -APIKey $APIKey -APISecret $APISecret
            }
            ElseIf (!$AuthToken) {
                $AuthToken = Get-NectarMSTeamsSubscription -TenantName $TenantName | Get-MSGraphAccessToken
            }
        }
        Catch {
            Write-Error "Could not obtain authorization token."
            Get-JSONErrorStream -JSONResponse $_        
        }
        
        If ($AuthToken) {
            $Headers = @{
                Authorization = "Bearer $AuthToken"
            }
            
            If ($ParticipantID) {
                $URI = "https://api.zoom.us/v2/metrics/meetings/$MeetingID/participants/$ParticipantID/qos"
                
                $Body = @{
                    type = $Type
                }
                $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Headers -Body $Body
                $JSON
            }
            Else {
                $URI = "https://api.zoom.us/v2/metrics/meetings/$MeetingID/participants/qos"
                
                $Body = @{
                    type = $Type
                    page_size = $PageSize
                }
                
                $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Headers -Body $Body
                $JSON.participants
                
                # If there is more than one page, use next_page_token to iterate through the pages
                While ($JSON.next_page_token) {
                    $Body.next_page_token = $JSON.next_page_token
                    $JSON = Invoke-RestMethod -Method GET -uri $URI -Headers $Headers -Body $Body
                    $JSON.participants
                }
            }
            Clear-Variable -Name AuthToken
        }
    } 
}



#################################################################################################################################################
# #
# Informational Functions #
# #
#################################################################################################################################################

Function Get-NectarCodecs {
    <#
        .SYNOPSIS
        Returns a list of Nectar 10 codecs used in calls
         
        .DESCRIPTION
        Returns a list of Nectar 10 codecs used in calls
 
        .PARAMETER TenantName
        The name of the Nectar 10 tenant. Used in multi-tenant configurations.
 
        .EXAMPLE
        Get-NectarCodecs
        Returns all codecs
         
        .NOTES
        Version 1.1
    #>

    
    [Alias("gnc")]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
            
            $JSON = Invoke-RestMethod -Method GET -Credential $Global:NectarCred -uri "https://$Global:NectarCloud/dapi/info/codecs?tenant=$TenantName"
            If (!$JSON) {
                Write-Error 'Codec not found.'
            }
            Else {
                Return $JSON
            }
        }
        Catch {
            Write-Error 'Unable to get codecs.'
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}




Function Get-NectarExtCities {
    <#
        .SYNOPSIS
        Returns a list of cities found via IP geolocation
         
        .DESCRIPTION
        Most call records include the user's external IP address. Nectar 10 does a geo-IP lookup of the external IP address and stores the geographic information for later use. This command will return all the cities where Nectar 10 was able to successfully geolocate an external IP address.
 
        .PARAMETER SearchQuery
        The name of the city to locate. Can be a partial match, and may return more than one entry.
 
        .PARAMETER TenantName
        The name of the Nectar 10 tenant. Used in multi-tenant configurations.
                 
        .PARAMETER ResultSize
        The number of results to return. Defaults to 1000.
 
        .EXAMPLE
        Get-NectarExtCities
        Returns the first 1000 cities sorted alphabetically.
 
        .EXAMPLE
        Get-NectarExtCities -ResultSize 5000
        Returns the first 5000 cities sorted alphabetically.
         
        .EXAMPLE
        Get-NectarExtCities -SearchQuery Gu
        Returns all cities that contain the letters 'gu'
         
        .NOTES
        Version 1.0
    #>

    
    [Alias("gneci")]
    Param (
        [Parameter(Mandatory=$False)]
        [string]$SearchQuery,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,100000)]
        [int]$ResultSize = 1000
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
            
            $URI = "https://$Global:NectarCloud/dapi/info/external/cities"
            $Params = @{ 'pageSize' = $ResultSize }    
            If ($SearchQuery) { $Params.Add('searchQuery',$SearchQuery) }
            If ($TenantName) { $Params.Add('Tenant',$TenantName) }
            
            $JSON = Invoke-RestMethod -Method GET -Credential $Global:NectarCred -uri $URI -Body $Params
            
            If ($TenantName) {$JSON.elements | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty}
            $JSON.elements
        }
        Catch {
            Write-Error "No results. Try specifying a less-restrictive filter"
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}




Function Get-NectarExtCountries {
    <#
        .SYNOPSIS
        Returns a list of 2-letter country codes found via IP geolocation
         
        .DESCRIPTION
        Most call records include the user's external IP address. Nectar 10 does a geo-IP lookup of the external IP address and stores the geographic information for later use. This command will return all the countries where Nectar 10 was able to successfully geolocate an external IP address.
 
        .PARAMETER SearchQuery
        The 2-letter country code to locate. Can be a partial match, and may return more than one entry.
 
        .PARAMETER TenantName
        The name of the Nectar 10 tenant. Used in multi-tenant configurations.
                 
        .PARAMETER ResultSize
        The number of results to return. Defaults to 1000.
 
        .EXAMPLE
        Get-NectarExtCountries
        Returns all country codes sorted alphabetically.
         
        .EXAMPLE
        Get-NectarExtCountries -SearchQuery US
        Returns all country codes that contain the letters 'US'
         
        .NOTES
        Version 1.0
    #>

    
    [Alias("gneco")]
    Param (
        [Parameter(Mandatory=$False)]
        [string]$SearchQuery,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,100000)]
        [int]$ResultSize = 1000
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
            
            $URI = "https://$Global:NectarCloud/dapi/info/external/countries"
            $Params = @{ 'pageSize' = $ResultSize }    
            If ($SearchQuery) { $Params.Add('searchQuery',$SearchQuery) }
            If ($TenantName) { $Params.Add('Tenant',$TenantName) }
            
            $JSON = Invoke-RestMethod -Method GET -Credential $Global:NectarCred -uri $URI -Body $Params
            
            If ($TenantName) {$JSON.elements | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty}
            $JSON.elements
        }
        Catch {
            Write-Error "No results. Try specifying a less-restrictive filter"
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}




Function Get-NectarExtISPs {
    <#
        .SYNOPSIS
        Returns a list of ISPs found via IP geolocation
         
        .DESCRIPTION
        Most call records include the user's external IP address. Nectar 10 does a geo-IP lookup of the external IP address and stores the geographic information for later use. This command will return all the ISPs where Nectar 10 was able to successfully geolocate an external IP address.
 
        .PARAMETER SearchQuery
        The name of the city to locate. Can be a partial match, and may return more than one entry.
 
        .PARAMETER TenantName
        The name of the Nectar 10 tenant. Used in multi-tenant configurations.
                 
        .PARAMETER ResultSize
        The number of results to return. Defaults to 1000.
 
        .EXAMPLE
        Get-NectarExtISPs
        Returns the first 1000 ISPs sorted alphabetically.
 
        .EXAMPLE
        Get-NectarExtISPs -ResultSize 5000
        Returns the first 5000 ISPs sorted alphabetically.
         
        .EXAMPLE
        Get-NectarExtISPs -SearchQuery Be
        Returns all ISPs that contain the letters 'be'
         
        .NOTES
        Version 1.0
    #>

    
    [Alias("gneci")]
    Param (
        [Parameter(Mandatory=$False)]
        [string]$SearchQuery,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$TenantName,
        [Parameter(Mandatory=$False)]
        [ValidateRange(1,100000)]
        [int]$ResultSize = 1000
    )
    
    Begin {
        Connect-NectarCloud
    }
    Process {
        Try {
            # Use globally set tenant name, if one was set and not explicitly included in the command
            If ($Global:NectarTenantName -And !$TenantName) { $TenantName = $Global:NectarTenantName }
            
            $URI = "https://$Global:NectarCloud/dapi/info/external/isps"
            $Params = @{ 'pageSize' = $ResultSize }    
            If ($SearchQuery) { $Params.Add('searchQuery',$SearchQuery) }
            If ($TenantName) { $Params.Add('Tenant',$TenantName) }
            
            $JSON = Invoke-RestMethod -Method GET -Credential $Global:NectarCred -uri $URI -Body $Params
            
            If ($TenantName) {$JSON.elements | Add-Member -Name 'TenantName' -Value $TenantName -MemberType NoteProperty}
            $JSON.elements
        }
        Catch {
            Write-Error "No results. Try specifying a less-restrictive filter"
            Get-JSONErrorStream -JSONResponse $_
        }
    }
}



#################################################################################################################################################
#################################################################################################################################################
## ##
## Endpoint Client Functions ##
## ##
#################################################################################################################################################
#################################################################################################################################################

#################################################################################################################################################
# #
# Controller Connection Functions #
# #
#################################################################################################################################################

Function Connect-EPCController {
    <#
        .SYNOPSIS
        Connects to EPC Controller and store the credentials for later use.
 
        .DESCRIPTION
        Connects to an Endpoint Client Controller and store the credentials for later use.
         
        .PARAMETER ControllerFQDN
        The FQDN of the Endpoint Client Controller
 
        .PARAMETER Credential
        The credentials used to access the EPC Controller.
         
        .PARAMETER StoredCredentialTarget
        Use stored credentials saved via New-StoredCredential. Requires prior installation of CredentialManager module via Install-Module CredentialManager, and running:
        Get-Credential | New-StoredCredential -Target MyEPCCreds -Persist LocalMachine
         
        .PARAMETER EnvFromFile
        Use a CSV file called EPCEnvList.csv located in the user's default Documents folder to show a list of environments to select from. Run [Environment]::GetFolderPath("MyDocuments") to find your default document folder.
        This parameter is only available if EPCEnvList.csv is found in the user's default Documents folder (ie: C:\Users\username\Documents)
        Also sets the default stored credential target to use for the selected environment. Requires prior installation and configuration of CredentialManager PS add-in.
        EPCEnvList.csv must have a header with two columns defined as "Environment, StoredCredentialTarget".
        Each environment and StoredCredentialTarget (if used) should be on their own separate lines
         
        .EXAMPLE
        $Cred = Get-Credential
        Connect-EPControllerFQDN -Credential $cred -ControllerFQDN contoso.nectar.services
        Connects to the contoso.nectar.services EPC Controller using the credentials supplied to the Get-Credential command
         
        .EXAMPLE
        Connect-EPControllerFQDN -ControllerFQDN contoso.nectar.services -StoredCredentialTarget MyEPCCreds
        Connects to contoso.nectar.services EPC Controller using previously stored credentials called MyEPCCreds
         
        .NOTES
        Version 1.0
    #>

    
    Param (
        [Parameter(ValueFromPipeline, Mandatory=$False)]
        # [ValidateScript ({
            # If ($_ -Match "^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.]+$") {
                # $True
            # }
            # Else {
                # Throw "ERROR: Endpoint Client Controller name must be in FQDN format."
            # }
        # })]
        [string] $ControllerFQDN,
        [Parameter(ValueFromPipelineByPropertyName)]
        [System.Management.Automation.Credential()]
        [PSCredential] $Credential,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$StoredCredentialTarget,
        [switch]$UseXML
    )
    DynamicParam {
        $DefaultDocPath = [Environment]::GetFolderPath("MyDocuments")
        $EnvPath = "$DefaultDocPath\EPCEnvList.csv"
        If (Test-Path $EnvPath -PathType Leaf) {
            # Set the dynamic parameters' name
            $ParameterName = 'EnvFromFile'
            
            # Create the dictionary
            $RuntimeParameterDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
         
            # Create the collection of attributes
            $AttributeCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
                    
            # Create and set the parameters' attributes
            $ParameterAttribute = New-Object System.Management.Automation.ParameterAttribute
            $ParameterAttribute.Mandatory = $False
            $ParameterAttribute.Position = 1
         
            # Add the attributes to the attributes collection
            $AttributeCollection.Add($ParameterAttribute)
         
            # Generate and set the ValidateSet
            $EnvSet = Import-Csv -Path $EnvPath
            $ValidateSetAttribute = New-Object System.Management.Automation.ValidateSetAttribute($EnvSet.Environment)
         
            # Add the ValidateSet to the attributes collection
            $AttributeCollection.Add($ValidateSetAttribute)
         
            # Create and return the dynamic parameter
            $RuntimeParameter = New-Object System.Management.Automation.RuntimeDefinedParameter($ParameterName, [string], $AttributeCollection)
            $RuntimeParameterDictionary.Add($ParameterName, $RuntimeParameter)
            Return $RuntimeParameterDictionary
        }
    }

    Begin {
        # Bind the dynamic parameter to a friendly variable
        If (Test-Path $EnvPath -PathType Leaf) {
            If ($PsBoundParameters[$ParameterName]) {
                $ControllerFQDN = $PsBoundParameters[$ParameterName]
                Write-Verbose "ControllerFQDN: $ControllerFQDN"
                
                # Get the array position of the selected environment
                $EnvPos = $EnvSet.Environment.IndexOf($ControllerFQDN)
                
                # Check for stored credential target in EPCEnvList.csv and use if available
                $StoredCredentialTarget = $EnvSet[$EnvPos].StoredCredentialTarget
                Write-Verbose "StoredCredentialTarget: $StoredCredentialTarget"
            }
        }
        <#
        The New-WebServiceProxy command is not supported on PS versions higher than 5.1.
        On PS 6.0+, we have to use Invoke-WebRequest, which works, but is more verbose and doesn't utilize the
        WSDL files which creates specific objects for the results.
        Do a check and set a global variable which will determine if the function will use New-WebServiceProxy or Invoke-WebRequest
        #>
 
        If ($PSVersionTable.PSVersion.Major -gt 5) {
            $Global:EPC_UseWSDL = $FALSE
        }
        Else {
            $Global:EPC_UseWSDL = $TRUE
        }
        Write-Verbose "Setting global UseWSDL variable to $Global:EPC_UseWSDL"
    }    
    Process {
        # Need to force TLS 1.2, if not already set
        If ([Net.ServicePointManager]::SecurityProtocol -ne 'Tls12') { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 }
        
        # Ask for the tenant name if global EPCController tenant variable not available and not entered on command line
        If ((-not $Global:EPControllerFQDN) -And (-not $ControllerFQDN)) {
            $ControllerFQDN = Read-Host "Enter the Endpoint Client Controller FQDN"
        }
        ElseIf (($Global:EPControllerFQDN) -And (-not $ControllerFQDN)) {
            $ControllerFQDN = $Global:EPControllerFQDN
        }
    
        $RegEx = "^(?:http(s)?:\/\/)?([\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\$&'\(\)\*\+,;=.]+)$"
        $FQDNMatch = Select-String -Pattern $Regex -InputObject $ControllerFQDN
        $EPControllerFQDN = $FQDNMatch.Matches.Groups[2].Value
    
        # Ask for credentials if global EPCController creds aren't available
        If (((-not $Global:EPControllerCred) -And (-not $Credential)) -Or (($Global:EPControllerFQDN -ne $EPControllerFQDN) -And (-Not $Credential)) -And (-Not $StoredCredentialTarget)) {
            $Credential = Get-Credential
        }
        ElseIf ($Global:EPControllerCred -And (-not $Credential)) {
            $Credential = $Global:EPControllerCred
        }
        
        # Pull stored credentials if specified
        If ($StoredCredentialTarget) {
            Try {
                $Credential = Get-StoredCredential -Target $StoredCredentialTarget
            }
            Catch {
                Write-Error "Cannot find stored credential for target: $StoredCredentialTarget"
            }
        }
        
        # Get the WSDL
        If ($Global:EPC_UseWSDL -And $UseXML -eq $False) {
            Write-Verbose 'Using WSDL'
            If ($Global:EPCWSDL_ActiveCtrl -and $Global:EPControllerFQDN -eq $EPControllerFQDN) {
                Write-Verbose 'Using WSDL from global variable'
                $EPC_ActiveCtrl = $Global:EPCWSDL_ActiveCtrl
                $EPC_ResGrpMgmt = $Global:EPCWSDL_ResGrpMgmt
                $EPC_SvcMgmt = $Global:EPCWSDL_SvcMgmt
            }
            ElseIf ((!$Global:EPCWSDL_ActiveCtrl -and $Global:EPControllerFQDN -eq $EPControllerFQDN) -Or !$Global:EPCWSDL_ActiveCtrl) {
                Write-Verbose "Loading WSDL from $EPControllerFQDN"
                $EPC_ActiveCtrl = New-WebServiceProxy "https://$EPControllerFQDN/telchemywebservices/services/telchemyActiveCtrlService?wsdl" -Namespace EPC.ActiveCtrl -Class ActiveCtrl 
                $EPC_ResGrpMgmt = New-WebServiceProxy "https://$EPControllerFQDN/telchemywebservices/services/telchemyRsrcGroupMgmtService?wsdl" -Namespace EPC.ResGrpMgmt -Class ResGrpMgmt
                $EPC_SvcMgmt = New-WebServiceProxy "https://$EPControllerFQDN/telchemywebservices/services/telchemySvcMgmtService?wsdl" -Namespace EPC.SvcMgmt -Class SvcMgmt
            }
            Else {
                Write-Error "There is already an active connection to $($Global:EPControllerFQDN). Please open a new PowerShell window to connect to a new controller."
                Return
            }
        }
                
        If ((-not $Global:EPControllerCred) -Or (-not $Global:EPControllerFQDN) -Or ($Global:EPControllerFQDN -ne $EPControllerFQDN)) {
            # Validate login credentials by running a simple API query
            If ($Global:EPC_UseWSDL -And $UseXML -eq $False) {
                # Store creds in EPC credential object
                $EPCCred = New-Object -TypeName EPC.ActiveCtrl.CredentialsType
                $EPCCred.username = $Credential.UserName
                $EPCCred.password = $Credential.GetNetworkCredential().Password

                Write-Verbose 'Using WSDL to test access'
                $EPCAPIVersionParams = New-Object -TypeName EPC.ActiveCtrl.GetAPIVersionParametersType
                $EPCAPIVersionParams.credentials = $EPCCred
                $EPCAPIVersion = $EPC_ActiveCtrl.getAPIVersion($EPCAPIVersionParams)
                
                $LoginResult = $EPCAPIVersion.result
            }
            Else {
                $ProgressPreference = 'SilentlyContinue'
                Write-Verbose 'Using WebXML to test access'
                [xml]$SOAPReq = "<soapenv:Envelope xmlns:soapenv='http://schemas.xmlsoap.org/soap/envelope/'
                    xmlns:urn='urn:telchemyActiveCtrl'>
                     <soapenv:Header/>
                     <soapenv:Body>
                     <urn:getAPIVersionParameters>
                    <credentials>
                    <username>$($Credential.UserName)</username>
                    <password>$($Credential.GetNetworkCredential().Password)</password>
                    </credentials>
                     </urn:getAPIVersionParameters>
                     </soapenv:Body>
                    </soapenv:Envelope>"


                Write-Verbose $SOAPReq.OuterXML
                $SOAPFQDN = "https://$EPControllerFQDN/telchemywebservices/services/telchemyActiveCtrlService"
                Write-Verbose $SOAPFQDN
                
                [xml]$XMLResponse = (Invoke-WebRequest -Method POST -URI $SOAPFQDN -Body $SOAPReq -ContentType 'text/xml').Content
                $LoginResult = $XMLResponse.Envelope.Body.getAPIVersionResults.result                
            }
            
            If ($LoginResult -ne 'success') {
                Write-Error "Could not connect to $EPControllerFQDN using $($Credential.UserName)"
                Throw $LoginResult
            }
            Else {
                Write-Host -ForegroundColor Green "Successful API connection to " -NoNewLine
                Write-Host -ForegroundColor Yellow "https://$EPControllerFQDN" -NoNewLine
                Write-Host -ForegroundColor Green " using " -NoNewLine
                Write-Host -ForegroundColor Yellow ($Credential).UserName
                $Global:EPControllerFQDN = $EPControllerFQDN
                $Global:EPControllerCred = $Credential
                $Global:EPCWSDL_ActiveCtrl = $EPC_ActiveCtrl
                $Global:EPCWSDL_ResGrpMgmt = $EPC_ResGrpMgmt
                $Global:EPCWSDL_SvcMgmt = $EPC_SvcMgmt

                If ($PSVersionTable.PSVersion.Major -gt 5) {
                    # Check for the PowerHTML module and install if not present. Needed for parsing web pageSize
                    If (!(Get-InstalledModule -Name PowerHTML)) { 
                        $Null = Install-Module -Name PowerHTML -Scope CurrentUser -Force
                    }
                }
                Write-Verbose "Setting global UseWSDL variable to $Global:EPC_UseWSDL"
            }
        }
    }
}    


Function Get-EPCConnectedController {
    <#
        .SYNOPSIS
        Returns the FQDN of the connected controller
 
        .DESCRIPTION
        Returns the FQDN of the connected controller
         
        .NOTES
        Version 1.0
    #>

    
    If ($Global:EPControllerFQDN) {
        Return $Global:EPControllerFQDN
    }
    Else {
        Return 'Not connected to a controller' 
    }
}



Function Get-EPCWebSessionCookie {
    <#
        .SYNOPSIS
        Creates a web session cookie for use with commands that can't use the EPC API
 
        .DESCRIPTION
        Connects to an Endpoint Client Controller and store the credentials for later use.
        Must have already successfully connected via Connect-EPCController
         
        .NOTES
        Version 1.0
    #>

    [cmdletbinding()]
    param ()

    Connect-EPCController
    $ProgressPreference = 'SilentlyContinue'
    
    # Check for existing global session cookie. Connect if not available and save the web session variable in a global variable
    If (!$Global:EPCSessionCookie) {
        Write-Verbose 'Global session cookie does not exist'
        $Dashboard = Invoke-WebRequest -UseBasicParsing -Uri "https://$EPControllerFQDN/dashboard.htm" -SessionVariable 'EPCSession'
        $SecurityCheck = Invoke-WebRequest -Uri "https://$EPControllerFQDN/j_security_check" -Method POST -Body "j_username=$($Global:EPControllerCred.UserName)&j_password=$($Global:EPControllerCred.GetNetworkCredential().Password)" -WebSession $EPCSession
        
        # Verify that session is active. We do this by looking for the existence of a single link on the dashboard page. If there is, this means the session expired and the base login page is being shown.
        # If there are numerous links, this indicates a successful login.
        $Dashboard = Invoke-WebRequest -UseBasicParsing -Uri "https://$EPControllerFQDN/dashboard.htm" -WebSession $EPCSession
        
        If ($Dashboard.Links.Count -gt 1) {
            Write-Host -ForegroundColor Green "Successful web session connection to " -NoNewLine
            Write-Host -ForegroundColor Yellow "https://$EPControllerFQDN" -NoNewLine
            Write-Host -ForegroundColor Green " using " -NoNewLine
            Write-Host -ForegroundColor Yellow ($Global:EPControllerCred).UserName
            $Global:EPCSessionCookie = $EPCSession
        }
        Else {
            Throw "Could not connect to $EPControllerFQDN web session using $($Credential.UserName)"
        }                
    }
    Else {
    # Verify that session is active. We do this by looking for the existence of a single link on the dashboard page. If there is, this means the session expired and the base login page is being shown.
    # If there are numerous links, this indicates the session is still active
    
        $Dashboard = Invoke-WebRequest -UseBasicParsing -Uri "https://$EPControllerFQDN/dashboard.htm" -WebSession $Global:EPCSessionCookie
        Write-Verbose "Session verify against $EPControllerFQDN"
        If ($Dashboard.Links.Count -eq 1) {
            # Re-authenticate and update global session cookie
            Write-Verbose 'Global session cookie expired'
            $Dashboard = Invoke-WebRequest -UseBasicParsing -Uri "https://$EPControllerFQDN/dashboard.htm" -SessionVariable 'EPCSession'
            $SecurityCheck = Invoke-WebRequest -Uri "https://$EPControllerFQDN/j_security_check" -Method POST -Body "j_username=$($Global:EPControllerCred.UserName)&j_password=$($Global:EPControllerCred.GetNetworkCredential().Password)" -WebSession $EPCSession
            $Dashboard = Invoke-WebRequest -UseBasicParsing -Uri "https://$EPControllerFQDN/dashboard.htm" -WebSession $EPCSession
            
            If ($Dashboard.Links.Count -gt 1) {
                Write-Verbose "Successful auth cookie regeneration against $EPControllerFQDN"
                $Global:EPCSessionCookie = $EPCSession
            }
            Else {
                Throw "Could not regenerate auth cookie against $EPControllerFQDN using $($Credential.UserName)"
            }
        }
        Else {
            Write-Verbose "Session verified against $EPControllerFQDN"
        }
    }
}



Function Disconnect-EPCController {
    <#
        .SYNOPSIS
        Disconnects from any active Endpoint Client Controller
         
        .DESCRIPTION
        Essentially deletes any stored credentials and FQDN from global variables.
        If PS version is less than 6, this doesn't currently work because there apparently isn't a way to remove the WSDL namespace from memory.
        Thiss means that connecting to a new EPC controller will still try to use the WSDL associated with the original controller.
        Any API commands will fail. Therefore, there is no useful reason to disconnect.
 
        .EXAMPLE
        Disconnect-EPCController
        Disconnects from all active connections to an EPC Controller
 
        .NOTES
        Version 1.0
    #>


    [cmdletbinding()]
    param ()

    $VariableNames = 'EPControllerCred','EPControllerFQDN','EPCSessionCookie','EPCWSDL_ActiveCtrl','EPCWSDL_ResGrpMgmt','EPCWSDL_SvcMgmt', 'EPC_UseWSDL'

    ForEach ($Variable in $VariableNames) {
        Remove-Variable $Variable -Scope Global -ErrorAction:SilentlyContinue
    }
}




#################################################################################################################################################
# #
# EPC Test Point Functions #
# #
#################################################################################################################################################

Function Get-EPCTestPoint {
    <#
        .SYNOPSIS
        Return information about a given EPC test point
         
        .DESCRIPTION
        Return information about a given EPC test point
         
        .PARAMETER Name
        The name of a specific test point to retrieve information about. Will do partial matches.
     
        .PARAMETER Status
        Only return information about test points with a specified status
         
        .PARAMETER OrgID
        Only return information about test points within a specified organization
         
        .PARAMETER Version
        Only return information about test points that match the given version
         
        .PARAMETER RGName
        Only return information about test points that are within a given resource group
        Can specify either RG name or ID
 
        .PARAMETER RGID
        Only return information about test points that are within a given resource group
        Can specify either RG name or ID
         
        .PARAMETER ResultSize
        The number of results to return. Defaults to 1000.
 
        .EXAMPLE
        Get-EPCTestPoint
        Returns information about the first 1000 test points
 
        .EXAMPLE
        Get-EPCTestPoint -Name 'TFerguson Laptop'
        Returns information about a specific test point using the test point name
         
        .EXAMPLE
        Get-EPCTestPoint -Status Connected -OrgID Contoso
        Returns information about connected test points in the Contoso organization
 
        .NOTES
        Version 1.0
    #>

    
    [cmdletbinding()]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$Name,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [ValidateSet('Connected','NotConnected','Reachable','Unknown','Unlicensed', IgnoreCase=$True)]
        [string]$Status,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$OrgID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$Version,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$RGName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [int[]]$RGID,
        [Parameter(Mandatory=$False)]
        [int]$ResultSize = 1000,
        [switch]$UseXML
    )
    
    Begin {
        Connect-EPCController
        
        If ($Global:EPC_UseWSDL -And $UseXML -eq $False) {
            $EPCCred = New-Object -TypeName EPC.ActiveCtrl.CredentialsType
            $EPCCred.username = $Global:EPControllerCred.UserName
            $EPCCred.password = $Global:EPControllerCred.GetNetworkCredential().Password
        }
        Else {
            $ProgressPreference = 'SilentlyContinue'
        }
    }
    Process {
        If ($Global:EPC_UseWSDL -And $UseXML -eq $False) {
            Write-Verbose 'Using WSDL'
            $EPCListTestPointParams = New-Object -TypeName EPC.ActiveCtrl.ListTestPointsParametersType
            $EPCListTestPointParams.credentials = $EPCCred
            $EPCListTestPointParams.maxNumber = $ResultSize

            # If any of the below parameters are specified, apply a filter
            If ($Name -Or $Status -Or $OrgID -Or $Version -Or $RGName -Or $RGID) {
                $EPCListTestPointFilterParams = New-Object -TypeName EPC.ActiveCtrl.ListTestPointsParametersTypeFilter
                If ($Name) { $EPCListTestPointFilterParams.namesubstring = $Name }
                If ($OrgID) { $EPCListTestPointFilterParams.orgID = $OrgID }
                If ($Version) { $EPCListTestPointFilterParams.version = $Version }
                If ($Status) { $EPCListTestPointFilterParams.status = $Status; $EPCListTestPointFilterParams.statusSpecified = $TRUE }
                If ($RGName) { $RGID = (Get-EPCResourceGroup | Where {$_.RGName -eq $RGName}).RGID }
                If ($RGID) { $EPCListTestPointFilterParams.RGList = $RGID }
                
                $EPCListTestPointParams.Filter = $EPCListTestPointFilterParams
            }
                        
            $TestPointResults = $Global:EPCWSDL_ActiveCtrl.listTestPoints($EPCListTestPointParams)
            Write-Verbose $TestPointResults.result

            If ($TestPointResults.result -eq 'success') {
                Return $TestPointResults.testptList
            }
            Else {
                Throw $TestPointResults.result
            }            
        }
        Else {
            Write-Verbose 'Using WebXML'
            [xml]$SOAPReq = "<soapenv:Envelope xmlns:soapenv='http://schemas.xmlsoap.org/soap/envelope/'
                xmlns:urn='urn:telchemyActiveCtrl'>
                 <soapenv:Header/>
                 <soapenv:Body>
                 <urn:listTestPointsParameters>
                <credentials>
                <username>$($Global:EPControllerCred.UserName)</username>
                <password>$($Global:EPControllerCred.GetNetworkCredential().Password)</password>
                </credentials>
                <maxNumber>$ResultSize</maxNumber>
                 </urn:listTestPointsParameters>
                 </soapenv:Body>
                </soapenv:Envelope>"


            # If any of the below parameters are specified, apply a filter
            If ($Name -Or $Status -Or $OrgID -Or $Version -Or $OSArch -Or $RGName -Or $RGID) {
                $FilterXMLElement = $SOAPReq.Envelope.Body.listTestPointsParameters.AppendChild($SOAPReq.CreateElement('filter'))
                
                If ($Name) { 
                    $NewXMLElement = $FilterXMLElement.AppendChild($SOAPReq.CreateElement('name-substring'))
                    $NULL = $NewXMLElement.AppendChild($SOAPReq.CreateTextNode($Name))
                }
                
                If ($Status) {
                    If ($Status -eq 'NotConnected') { $StatusFormatted = 'not connected' } Else { $StatusFormatted = $Status }
                    $NewXMLElement = $FilterXMLElement.AppendChild($SOAPReq.CreateElement('status'))
                    $NULL = $NewXMLElement.AppendChild($SOAPReq.CreateTextNode($StatusFormatted.ToLower()))                    
                }
                
                If ($OrgID) {
                    $NewXMLElement = $FilterXMLElement.AppendChild($SOAPReq.CreateElement('orgID'))
                    $NULL = $NewXMLElement.AppendChild($SOAPReq.CreateTextNode($OrgID.ToLower()))                    
                }

                If ($Version) {
                    $NewXMLElement = $FilterXMLElement.AppendChild($SOAPReq.CreateElement('version'))
                    $NULL = $NewXMLElement.AppendChild($SOAPReq.CreateTextNode($Version))                
                }
                
                If ($RGName) { 
                    $RGID = (Get-EPCResourceGroup | Where {$_.RGName -eq $RGName}).RGID
                }
                
                If ($RGID) { 
                    $RGXMLElement = $FilterXMLElement.AppendChild($SOAPReq.CreateElement('RGList'))
                    $NewXMLElement = $RGXMLElement.AppendChild($SOAPReq.CreateElement('rg'))
                    $NULL = $NewXMLElement.AppendChild($SOAPReq.CreateTextNode($RGID))                
                }
            }

            Write-Verbose $SOAPReq.OuterXML
            $SOAPFQDN = "https://$Global:EPControllerFQDN/telchemywebservices/services/telchemyActiveCtrlService"
            Write-Verbose $SOAPFQDN
            
            [xml]$XMLResponse = (Invoke-WebRequest -Method POST -URI $SOAPFQDN -Body $SOAPReq -ContentType 'text/xml').Content
            $TestPointResults = $XMLResponse.Envelope.Body.listTestPointsResults
            
            If ($TestPointResults.result -eq 'success') {
                Return $TestPointResults.testptList.testpt
            }
            Else {
                Throw $TestPointResults.result
            }
        }        
    }
}



Function Set-EPCTestPoint {
    <#
        .SYNOPSIS
        Update the name and/or description for a given EPC test point
         
        .DESCRIPTION
        Update the name and/or description for a given EPC test point
         
        .PARAMETER UUID
        The UUID of the test point to update
         
        .PARAMETER Name
        The display name to set on the test point
         
        .PARAMETER Description
        The description to set on the test point
 
        .EXAMPLE
        Set-EPCTestPoint -UUID d4a1437f-1f18-11ec-cf33-0daedf04882a -Name 'New Name' -Description 'Updated description'
        Updates the selected test point's name and description
 
        .NOTES
        Version 1.0
    #>

    
    [cmdletbinding()]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [Alias("ID")]
        [string]$UUID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [Alias("UserDisplayName")]
        [string]$DisplayName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$Description    
    )
    
    Begin {
        Get-EPCWebSessionCookie
        $ProgressPreference = 'SilentlyContinue'
    }
    Process {
        $TPFQDN = "https://$EPControllerFQDN/test/testPoint.do?action=2"
        
        # If name isn't set, pull the name from the existing record, otherwise it will replace the name with its IP address
        If (!$DisplayName) { $DisplayName = (Get-EPCTestPoint -UUID $UUID).Name }
        
        $TPBody = @{
            tptype = 1
            uuid = $UUID
            name = $DisplayName
        }
        
        If ($Description) { $TPBody.Add('description',$Description -Replace "[^\sa-zA-Z0-9.-]") }

        Write-Verbose "UUID: $UUID, Name: $DisplayName"
        
        $TPResult = Invoke-WebRequest -Method POST -Uri $TPFQDN -Body $TPBody -WebSession $Global:EPCSessionCookie
    }
}




Function Get-EPCTestPointInterface {
    <#
        .SYNOPSIS
        Return network interface inforemation for a given EPC test point
         
        .DESCRIPTION
        Return network interface inforemation for a given EPC test point
         
        .PARAMETER ID
        The ID of the test point to retrieve information about
         
        .EXAMPLE
        Get-EPCTestPointInterface -ID d4a1437f-1f18-11ec-cf33-0daedf04882a
        Returns network interface information about a specific test point
 
        .NOTES
        Version 1.0
    #>


    [cmdletbinding()]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [Alias("testptID")]
        [string]$UUID,
        [switch]$UseXML
    )
    
    Begin {
        Connect-EPCController
        
        If ($Global:EPC_UseWSDL -And $UseXML -eq $False) {
            $EPCCred = New-Object -TypeName EPC.ActiveCtrl.CredentialsType
            $EPCCred.username = $Global:EPControllerCred.UserName
            $EPCCred.password = $Global:EPControllerCred.GetNetworkCredential().Password
        }
        Else {
            $ProgressPreference = 'SilentlyContinue'
        }
    }
    Process {
        If ($Global:EPC_UseWSDL -And $UseXML -eq $False) {
            Write-Verbose 'Using WSDL to pull interface list'
            $EPCListTestPointInterfaceParams = New-Object -TypeName EPC.ActiveCtrl.ListTestPointInterfaceParametersType
            $EPCListTestPointInterfaceParams.credentials = $EPCCred
            $EPCListTestPointInterfaceParams.testptID = $UUID
            $TestPointInterfaceResults = $Global:EPCWSDL_ActiveCtrl.listTestPointInterfaces($EPCListTestPointInterfaceParams)
            Write-Verbose $TestPointInterfaceResults.result
            
            If ($TestPointInterfaceResults.result -eq 'success') {
                Return $TestPointInterfaceResults.InterfaceList
            }
            Else {
                Throw $TestPointInterfaceResults.result
            }
        }
        Else {
            Write-Verbose 'Using XML to pull interface list'
            [xml]$SOAPReq = "<soapenv:Envelope xmlns:soapenv='http://schemas.xmlsoap.org/soap/envelope/'
                xmlns:urn='urn:telchemyActiveCtrl'>
                 <soapenv:Header/>
                 <soapenv:Body>
                 <urn:listTestPointInterfaceParameters>
                <credentials>
                <username>$($Global:EPControllerCred.UserName)</username>
                <password>$($Global:EPControllerCred.GetNetworkCredential().Password)</password>
                </credentials>
                <testptID>$UUID</testptID>
                 </urn:listTestPointInterfaceParameters>
                 </soapenv:Body>
                </soapenv:Envelope>"


            $SOAPFQDN = "https://$Global:EPControllerFQDN/telchemywebservices/services/telchemyActiveCtrlService"
            
            [xml]$XMLResponse = (Invoke-WebRequest -Method POST -URI $SOAPFQDN -Body $SOAPReq -ContentType 'text/xml').Content
            $TestPointInterfaceResults = $XMLResponse.Envelope.Body.listTestPointInterfaceResults
            
            If ($TestPointInterfaceResults.result -eq 'success') {
                [psobject]$TestPointInterfaces = $TestPointInterfaceResults.InterfaceList.interface | ConvertFrom-XMLElement
                Return $TestPointInterfaces
            }
            Else {
                Throw $TestPointInterfaceResults.result
            }
        }
    }
}





#################################################################################################################################################
# #
# EPC Test Group Functions #
# #
#################################################################################################################################################

Function Get-EPCTestGroups {
    <#
        .SYNOPSIS
        Returns a list of EPC test groups and their associated IDs
         
        .DESCRIPTION
        Returns a list of EPC test groups and their associated IDs
 
        .PARAMETER ResultSize
        The number of results to return. Defaults to 1000.
         
        .EXAMPLE
        Get-EPCTestGroups
 
        .NOTES
        Version 1.0
    #>

    
    [cmdletbinding()]
    Param (
        [Parameter(Mandatory=$False)]
        [int]$ResultSize = 1000,
        [switch]$UseXML
    )
    
    Begin {
        Get-EPCWebSessionCookie
        $ProgressPreference = 'SilentlyContinue'
    }
    Process {
        $TGFQDN = "https://$Global:EPControllerFQDN/test/testGroup.do?action=14&numRecs=$ResultSize"
        $TGResult = Invoke-WebRequest -Method GET -Uri $TGFQDN -WebSession $Global:EPCSessionCookie
        
        # Parse out tables and focus on the 4th table which contains a list of all the test groups
        If ($Global:EPC_UseWSDL -And $UseXML -eq $False) {
            $Tables = $TGResult.ParsedHtml.body.getElementsByTagName('Table')
            $TestGroupRows = $Tables[3].rows
            $RowCount = $TestGroupRows.length - 1
            $RowStart = 3
        }
        Else { # PS v.6+ doesn't have ParsedHtml in Invoke-WebRequest, so we have to use different method
            $Tables = $TGResult.Content | ConvertFrom-Html
            $Tables = $Tables.SelectNodes("//table")
            $TestGroupRows = $Tables[3].SelectNodes("//tr")
            $RowCount = $TestGroupRows.Count - 1
            $RowStart = 4
        }
        

        $TestGroups = @()
        $RegEx = "viewtestgroup.htm\?tgid\=(\d+)"
        For ($RowNum=$RowStart; $RowNum -lt $RowCount; $RowNum++) {
            # Parse the Test Group ID out of the InnerHTML
            Write-Verbose "TestGroup: $($TestGroupRows.Item($RowNum).InnerText)"
            $FQDNMatch = Select-String -Pattern $Regex -InputObject $TestGroupRows.Item($RowNum).InnerHTML
            $TGID = $FQDNMatch.Matches.Groups[1].Value
            
            $Item = [PSCustomObject][Ordered]@{
                Name = $TestGroupRows.Item($RowNum).InnerText
                TGID = $TGID
            }
            $TestGroups += $Item
        }
                
        If ($TestGroups) {
            Return $TestGroups
        }
        Else {
            Write-Error "Could not find any test groups"
        }
    }
}



Function Get-EPCTestGroupType {
    <#
        .SYNOPSIS
        Returns the group type and group type ID of a test group
         
        .DESCRIPTION
        Returns the group type and group type ID of a test group
         
        .PARAMETER Name
        The name of the test group to return the type and ID
         
        .EXAMPLE
        Get-EPCTestGroupType -Name 'TG1'
        Returns the group type and ID of the TG1 test group
 
        .NOTES
        Version 1.0
    #>

    
    [cmdletbinding()]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [string]$Name,
        [switch]$UseXML
    )
    
    Begin {
        Get-EPCWebSessionCookie
        $ProgressPreference = 'SilentlyContinue'
    }
    Process {
        $TGID = (Get-EPCTestGroups | Where {$_.Name -eq $Name}).TGID
        
        If (!$TGID) { Throw "Cound not find a test group with name $Name" }
        
        $TGFQDN = "https://$Global:EPControllerFQDN/test/edittestgroup.htm?testgroupid=$TGID"
        $TGResult = Invoke-WebRequest -Method GET -Uri $TGFQDN -WebSession $Global:EPCSessionCookie
        
        # Find the row where Group Type is mentioned and get the group type
        If ($Global:EPC_UseWSDL -And $UseXML -eq $False) {
            $Tables = $TGResult.ParsedHtml.body.getElementsByTagName('Table')
            $TGType = ($Tables[0].Rows | Where {$_.innerText -like 'Group Type*'}).InnerText
        }
        Else { # PS v.6+ doesn't have ParsedHtml in Invoke-WebRequest, so we have to use different method
            $Tables = $TGResult.Content | ConvertFrom-Html
            $Tables = $Tables.SelectNodes("//table")
            $TGType = ($Tables[0].SelectNodes("//tr") | Where {$_.innerText -like '*Group Type*'}).InnerText
        }

        $RegEx = "Group Type:\W*(\w*)"
        $FQDNMatch = Select-String -Pattern $RegEx -InputObject $TGType
        $TGType = $FQDNMatch.Matches.Groups[1].Value
            
        Switch ($TGType) {
            'Network Entity' { $TGTypeID = 0 }
            'Agent' { $TGTypeID = 1 }
            'DHCP' { $TGTypeID = 2 }
            'DNS' { $TGTypeID = 3 }
            'HTTP' { $TGTypeID = 4 }
            'POP3' { $TGTypeID = 5 }
            'SIP Endpoint' { $TGTypeID = 6 }
            'SMTP' { $TGTypeID = 7 }
        }
                
        If ($TGTypeID) {
            $TGTypeInfo = [PSCustomObject][Ordered]@{
                ID = $TGTypeID
                Type = $TGType
            }
            Return $TGTypeInfo
        }
        Else {
            Throw "Could not find a test group type with name $TGType"
        }
    }
}



Function Set-EPCTestGroup {
    <#
        .SYNOPSIS
        Update the name of an existing test group
         
        .DESCRIPTION
        Update the name of an existing test group
         
        .PARAMETER Name
        The name of the test group to update
 
        .PARAMETER NewName
        The new name of the test group
         
        .PARAMETER TGID
        The ID of a test group. If both Name and TGID are specified, Name will take priority.
         
        .EXAMPLE
        Set-EPCTestGroup -Name 'Existing Name' -NewName 'New Name'
 
        .NOTES
        Version 1.0
    #>

    
    [cmdletbinding()]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$Name,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$NewName,        
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [int]$TGID
    )
    
    Begin {
        Get-EPCWebSessionCookie
        $ProgressPreference = 'SilentlyContinue'
    }
    Process {
        If ($Name) { $TGID = (Get-EPCTestGroups | Where {$_.Name -eq $Name}).TGID }
        
        $TGFQDN = "https://$Global:EPControllerFQDN/test/testGroup.do?action=4"
        
        $TGBody = @{
            testgroupid = $TGID
            testgroupname = $NewName
        }
        
        $TGResult = Invoke-WebRequest -Method POST -Uri $TGFQDN -Body $TGBody -WebSession $Global:EPCSessionCookie
    }
}



Function New-EPCTestGroup {
    <#
        .SYNOPSIS
        Create a new EPC test group
         
        .DESCRIPTION
        Create a new EPC test group
         
        .PARAMETER Name
        The name of the test group
         
        .PARAMETER Type
        The type to assign to the test group
         
        .EXAMPLE
        New-EPCTestGroup -Name MyGroup -Type Agent
        Creates an agent test group called MyGroup
 
        .NOTES
        Version 1.0
    #>

    
    [cmdletbinding()]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [string]$Name,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [ValidateSet('Agent','Network Entity','DHCP','DNS','HTTP','POP3','SIP Endpoint','SMTP', IgnoreCase=$True)]
        [string]$Type = 'Agent'
    )
    
    Begin {
        Get-EPCWebSessionCookie
        $ProgressPreference = 'SilentlyContinue'
    }
    Process {
        $TGFQDN = "https://$Global:EPControllerFQDN/test/testGroup.do?action=3"
        
        Switch ($Type) {
            'Network Entity' { $TGTypeID = 0 }
            'Agent' { $TGTypeID = 1 }
            'DHCP' { $TGTypeID = 2 }
            'DNS' { $TGTypeID = 3 }
            'HTTP' { $TGTypeID = 4 }
            'POP3' { $TGTypeID = 5 }
            'SIP Endpoint' { $TGTypeID = 6 }
            'SMTP' { $TGTypeID = 7 }
        }
        
        $TGBody = @{
            testgroupname = $Name
            testgrouptype = $TGTypeID
        }
        
        $TGResult = Invoke-WebRequest -Method POST -Uri $TGFQDN -Body $TGBody -WebSession $Global:EPCSessionCookie
    }
}



Function Remove-EPCTestGroup {
    <#
        .SYNOPSIS
        Removes an existing EPC test group
         
        .DESCRIPTION
        Removes an existing EPC test group
         
        .EXAMPLE
        Remove-EPCTestGroup -Name 'My Test Group'
 
        .NOTES
        Version 1.0
    #>

    
    [cmdletbinding()]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$Name,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [int]$TGID
    )
    
    Begin {
        Get-EPCWebSessionCookie
        $ProgressPreference = 'SilentlyContinue'
    }
    Process {
        If ($Name) { $TGID = (Get-EPCTestGroups | Where {$_.Name -eq $Name}).TGID }
        
        $TGFQDN = "https://$Global:EPControllerFQDN/test/testGroup.do?action=6&groupid=$TGID"
        
        $TGResult = Invoke-WebRequest -Method GET -Uri $TGFQDN -WebSession $Global:EPCSessionCookie
    }
}



Function Get-EPCTestGroupMembers {
    <#
        .SYNOPSIS
        Returns a list of test points associated with a given test group
         
        .DESCRIPTION
        Returns a list of test points associated with a given test group
         
        .EXAMPLE
        Get-EPCTestGroupMembers -ID 4
 
        .NOTES
        Version 1.0
    #>

    
    [cmdletbinding()]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$Name,        
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [int]$TGID,
        [switch]$UseXML    
    )
    
    Begin {
        Get-EPCWebSessionCookie
        $ProgressPreference = 'SilentlyContinue'
    }
    Process {
        If ($Name) { $TGID = (Get-EPCTestGroups | Where {$_.Name -eq $Name}).TGID }
        If ($TGID) { $Name = (Get-EPCTestGroups | Where {$_.TGID -eq $TGID}).Name }
        
        [System.Collections.ArrayList]$TGMembers = @()
        $TGFQDN = "https://$Global:EPControllerFQDN/test/viewtestgroup.htm?tgid=$TGID"
        $TGResult = Invoke-WebRequest -Method GET -Uri $TGFQDN -WebSession $Global:EPCSessionCookie
        
        # Parse out tables and focus on the 2nd table which contains a list of all the test group members
        If ($Global:EPC_UseWSDL -And $UseXML -eq $False) {
            Write-Verbose 'Using WSDL method'
            $Tables = $TGResult.ParsedHtml.body.getElementsByTagName('Table')
            $TGMemberRows = $Tables[1].rows
            $RowCount = $TGMemberRows.length - 1
            $RowStart = 2
            
            For ($RowNum = $RowStart; $RowNum -lt $RowCount; $RowNum++) {
                $Item = [PSCustomObject][Ordered]@{
                    testPointName = $TGMemberRows.Item($RowNum).cells[0].InnerText
                    groupName = $TGMemberRows.Item($RowNum).cells[1].InnerText
                    Type = $TGMemberRows.Item($RowNum).cells[2].InnerText
                    testInterfaceName = $TGMemberRows.Item($RowNum).cells[3].InnerText
                    tpIp = $TGMemberRows.Item($RowNum).cells[4].InnerText
                    TestGroupName = $Name
                }
                $TGMembers += $Item
            }
        }
        Else { # PS v.6+ doesn't have ParsedHtml in Invoke-WebRequest, so we have to use different method
            $Tables = $TGResult.Content | ConvertFrom-Html
            $Tables = $Tables.SelectNodes("//table")
            $TGMemberRows = $Tables[1].SelectNodes("//tr")
            $RowCount = $TGMemberRows.Count
            $RowStart = 4
            
            For ($RowNum = $RowStart; $RowNum -lt $RowCount; $RowNum++) {
                $Item = [PSCustomObject][Ordered]@{
                    testPointName = $TGMemberRows.Item($RowNum).SelectNodes("/tr|td")[0].InnerText
                    groupName = $TGMemberRows.Item($RowNum).SelectNodes("/tr|td")[1].InnerText -Replace '&#x2f;', '/'
                    Type = $TGMemberRows.Item($RowNum).SelectNodes("/tr|td")[2].InnerText
                    testInterfaceName = $TGMemberRows.Item($RowNum).SelectNodes("/tr|td")[3].InnerText
                    tpIp = $TGMemberRows.Item($RowNum).SelectNodes("/tr|td")[4].InnerText
                    TestGroupName = $Name
                }
                $TGMembers += $Item
            }
        }        
        
        If ($TGMembers) {
            Return $TGMembers
        }
        Else {
            Throw "Could not find any test group members"
        }
    }
}



Function Add-EPCTestGroupMember {
    <#
        .SYNOPSIS
        Adds an EPC test point to an EPC test group
         
        .DESCRIPTION
        Adds an EPC test point to an EPC test group
         
        .PARAMETER TestGroupName
        The name of the test group to add the member to
         
        .PARAMETER ResourceGroupName
        The name of the resource group associated with this test group
         
        .PARAMETER TestPointName
        The name of the test point to add to the test group
         
        .PARAMETER InterfaceID
        The interface ID of the test point
         
        .PARAMETER IPAddress
        The IP address associated with the given interface
         
        .EXAMPLE
        Add-EPCTestGroupMember -TestGroupName MyTestGroup -ResourceGroupName MyRG -TestPointName 'TFerguson laptop' -InterfaceID 'Primary Network Adapter' -IPAddress '192.168.1.20'
        Adds the given test point to the MyTestGroup test group
 
        .NOTES
        Version 1.0
    #>

    
    [cmdletbinding()]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [string]$TestGroupName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [string]$ResourceGroupName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [Alias('name')]
        [string]$TestPointName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [string]$InterfaceID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [string]$IPAddress
    )
    
    Begin {
        Get-EPCWebSessionCookie
        $ProgressPreference = 'SilentlyContinue'
        
        # Get a list of all test points and resource groups for returning IDs
        $TPList = Get-EPCTestPoint -ResultSize 10000
        $RGList = Get-EPCResourceGroup
    }
    Process {
        # Get the test group ID
        [int]$TestGroupID = (Get-EPCTestGroups | Where {$_.Name -eq $TestGroupName}).TGID
        If (!$TestGroupID) {
            Throw "Could not find a test group with name $TestGroupName"
        }
        
        # Get the resource group ID
        $TargetRG = $RGList | Where {$_.RGName -eq $ResourceGroupName}
        $RGID = $TargetRG.RGID
        [string]$RGName = $TargetRG.RGName
        
        [int]$RGParentID = $TargetRG.RGParentID
        
        While ($RGParentID -ne 0) {
            $NewParent = $RGList | Where {$_.RGID -eq $RGParentID}
            $RGName = "$($NewParent.RGName) / $RGName"
            Write-Verbose $RGName
            [int]$RGParentID = $NewParent.RGParentID
        }
        
        Write-Verbose "RGName: $RGName"
        Write-Verbose "RGID: $RGID"
        
        # Obtain list of existing group members
        [System.Collections.ArrayList]$TGMembers = @(Get-EPCTestGroupMembers -Name $TestGroupName | Select-Object TestPointName, GroupName, TestInterfaceName, TPIP)
        
        # Add fields for resource group and test point IDs
        $TGMembers | Add-Member -NotePropertyName 'resgroupid' -NotePropertyValue $NULL
        $TGMembers | Add-Member -NotePropertyName 'testPointId' -NotePropertyValue $NULL
        
        #Add the RGID and TPID for each entry
        ForEach ($Member in $TGMembers) {
            $Member.TestPointID = ($TPList | Where {$_.Name -eq $Member.TestPointName}).TestPtID
            
            # Parse the RG Name which may be in parent / child format
            $RegEx = "/?(\w+)$"
            $FQDNMatch = Select-String -Pattern $Regex -InputObject $Member.GroupName
            $EPCGroupName = $FQDNMatch.Matches.Groups[1].Value
            
            $Member.ResGroupID = ($RGList | Where {$_.RGName -eq $EPCGroupName}).RGID
        }

        $NewItem = [PSCustomObject][Ordered]@{
            testPointName = $TestPointName
            groupName = $ResourceGroupName
            testInterfaceName = $InterfaceID
            tpIp = $IPAddress
            resgroupid = $RGID
            testPointId = ($TPList | Where {$_.Name -eq $TestPointName}).TestPtID
        }
        
        $TGMembers += $NewItem
    }
    End {
        # Convert test point array to XML
        $TGMemberXML = New-EPCTestPointXML -Data $TGMembers
        
        $TGFQDN = "https://$Global:EPControllerFQDN/test/testGroup.do?action=5"
        
        [int]$TestGroupType = (Get-EPCTestGroupType -Name $TestGroupName).ID
        
        Write-Verbose "XML"
        Write-Verbose $TGMemberXML
        
        $TGBody = @{
            testpointlist = $TGMemberXML
            testgroupid = $TestGroupID
            testgrouptype = $TestGroupType
            dstgrouptype = $TestGroupType
            resgrp1 = $RGID
        }
    
        Write-Verbose 'Writing changes to controller database'
        
        $TGResult = Invoke-WebRequest -Method POST -Uri $TGFQDN -Body $TGBody -WebSession $Global:EPCSessionCookie
    }
}



Function Remove-EPCTestGroupMember {
    <#
        .SYNOPSIS
        Removes an EPC test point from an EPC test group
         
        .DESCRIPTION
        Removes an EPC test point from an EPC test group
         
        .PARAMETER TestGroupName
        The name of the test group to remove the member from
         
        .PARAMETER TestPointName
        The name of the test point to remove from the test group
         
        .EXAMPLE
        Remove-EPCTestGroupMember -TestGroupName MyGroup -TestPointName TPTest
        Removes TPTest from the MyGroup group
 
        .NOTES
        Version 1.0
    #>

    
    [cmdletbinding()]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [string]$TestGroupName,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [string]$TestPointName
    )
    
    Begin {
        Get-EPCWebSessionCookie
        $ProgressPreference = 'SilentlyContinue'
        
        # Get a list of all test points and resource groups for returning IDs
        $TPList = Get-EPCTestPoint -ResultSize 10000
        $RGList = Get-EPCResourceGroup
    }
    Process {
        # Get the test group ID
        [int]$TestGroupID = (Get-EPCTestGroups | Where {$_.Name -eq $TestGroupName}).TGID
        If (!$TestGroupID) {
            Throw "Could not find a test group with name $TestGroupName"
        }
        
        # Get the resource group ID
        $RGID = ($RGList | Where {$_.RGName -eq $ResourceGroupName}).RGID
        
        # Obtain list of existing group members
        [System.Collections.ArrayList]$TGMembers = Get-EPCTestGroupMembers -Name $TestGroupName | Select-Object TestPointName, GroupName, TestInterfaceName, TPIP
        
        # Delete the selected test point from the table of existing members
        $MemberDelete = $TGMembers | Where {$_.TestPointName -eq $TestPointName}
        
        If ($MemberDelete) {
            $TGMembers.Remove($MemberDelete)
        }
        Else {
            Throw "Could not find $TestPointName in $TestGroupName"
        }
        
        # Add fields for resource group and test point IDs
        $TGMembers | Add-Member -NotePropertyName 'resgroupid' -NotePropertyValue $NULL
        $TGMembers | Add-Member -NotePropertyName 'testPointId' -NotePropertyValue $NULL
        
        #Add the RGID and TPID for each entry
        ForEach ($Member in $TGMembers) {
            $Member.TestPointID = ($TPList | Where {$_.Name -eq $Member.TestPointName}).TestPtID
            $Member.ResGroupID = ($RGList | Where {$_.RGName -eq $Member.GroupName}).RGID
        }
    }
    End {
        # Convert test point array to XML
        $TGMemberXML = New-EPCTestPointXML -Data $TGMembers
        
        $TGFQDN = "https://$Global:EPControllerFQDN/test/testGroup.do?action=5"
        
        [int]$TestGroupType = (Get-EPCTestGroupType -Name $TestGroupName).ID
        
        $TGBody = @{
            testpointlist = $TGMemberXML
            testgroupid = $TestGroupID
            testgrouptype = $TestGroupType
            dstgrouptype = $TestGroupType
            resgrp1 = $RGID
        }
        
        Write-Verbose 'Writing to controller database'
        
        $TGResult = Invoke-WebRequest -Method POST -Uri $TGFQDN -Body $TGBody -WebSession $Global:EPCSessionCookie
    }
}



#################################################################################################################################################
# #
# EPC Test Plan Functions #
# #
#################################################################################################################################################

Function Get-EPCTestPlan {
    <#
        .SYNOPSIS
        Return a list of EPC test plans
         
        .DESCRIPTION
        Return a list of EPC test plans
         
        .EXAMPLE
        Get-EPCTestPlan
 
        .NOTES
        Version 1.0
    #>

    
    [cmdletbinding()]
    Param (
        [switch]$UseXML
    )
    
    Begin {
        Connect-EPCController

        If ($Global:EPC_UseWSDL -And $UseXML -eq $False) {
            $EPCCred = New-Object -TypeName EPC.ActiveCtrl.CredentialsType
            $EPCCred.username = $Global:EPControllerCred.UserName
            $EPCCred.password = $Global:EPControllerCred.GetNetworkCredential().Password
        }
        Else {
            $ProgressPreference = 'SilentlyContinue'
        }
    }
    Process {
        If ($Global:EPC_UseWSDL -And $UseXML -eq $False) {
            $EPCListTestPlansParams = New-Object -TypeName EPC.ActiveCtrl.ListTestPlansParametersType
            $EPCListTestPlansParams.credentials = $EPCCred

            $TestPlanResults = $Global:EPCWSDL_ActiveCtrl.listTestPlans($EPCListTestPlansParams)
            
            Write-Verbose $TestPlanResults.result
            
            If ($TestPlanResults.result -eq 'success') {
                Return $TestPlanResults.TestPlanList
            }
            Else {
                Throw $TestPlanResults.result
            }
        }
        Else {
            [xml]$SOAPReq = "<soapenv:Envelope xmlns:soapenv='http://schemas.xmlsoap.org/soap/envelope/'
                xmlns:urn='urn:telchemyActiveCtrl'>
                 <soapenv:Header/>
                 <soapenv:Body>
                 <urn:listTestPlansParameters>
                <credentials>
                <username>$($Global:EPControllerCred.UserName)</username>
                <password>$($Global:EPControllerCred.GetNetworkCredential().Password)</password>
                </credentials>
                 </urn:listTestPlansParameters>
                 </soapenv:Body>
                </soapenv:Envelope>"


            $SOAPFQDN = "https://$Global:EPControllerFQDN/telchemywebservices/services/telchemyActiveCtrlService"
            
            [xml]$XMLResponse = (Invoke-WebRequest -Method POST -URI $SOAPFQDN -Body $SOAPReq -ContentType 'text/xml').Content
            $TestPlanResults = $XMLResponse.Envelope.Body.listTestPlansResults
            
            If ($TestPlanResults.result -eq 'success') {
                [psobject]$TestPlans = $TestPlanResults.TestPlanList.testplan | ConvertFrom-XMLElement
                Return $TestPlans
            }
            Else {
                Throw $TestPlanResults.result
            }
        }
    }
}



#################################################################################################################################################
# #
# EPC Resource Group Functions #
# #
#################################################################################################################################################

Function Get-EPCResourceGroup {
    <#
        .SYNOPSIS
        Return list of EPC resource groups
         
        .DESCRIPTION
        Return list of EPC resource groups
         
        .PARAMETER ServiceClass
        Limit results to only a specific service class
         
        .EXAMPLE
        Get-EPCService -ServiceClass VOIP
        Returns service information about all VOIP services
 
        .NOTES
        Version 1.0
    #>

    
    [cmdletbinding()]
    Param (
        [Parameter(Mandatory=$False)]
        [int]$ParentRGID,
        [Parameter(Mandatory=$False)]
        [int]$ResultSize = 1000,
        [switch]$UseXML
    )
    Begin {
        Connect-EPCController
        
        If ($Global:EPC_UseWSDL -And $UseXML -eq $False) {
            $EPCCred = New-Object -TypeName EPC.ResGrpMgmt.CredentialsType
            $EPCCred.username = $Global:EPControllerCred.UserName
            $EPCCred.password = $Global:EPControllerCred.GetNetworkCredential().Password
        }
        Else {
            $ProgressPreference = 'SilentlyContinue'
        }
    }
    Process {
        If ($Global:EPC_UseWSDL -And $UseXML -eq $False) {
            $EPCListResourceGroupParams = New-Object -TypeName EPC.ResGrpMgmt.ListResourceGroupParametersType
            $EPCListResourceGroupParams.credentials = $EPCCred
            $EPCListResourceGroupParams.maxNumber = $ResultSize
            
            If ($ParentRGID) {
                $RGRelation = New-Object -TypeName EPC.ResGrpMgmt.ResourceGroupRelationshipMapType
                $RGRelation.RGID = $ParentRGID
                $FilterType = New-Object -TypeName EPC.ResGrpMgmt.ResourceGroupFilterType
                $FilterType.ItemElementName = 'RGrelation'
                $FilterType.Item = $RGRelation
                $EPCListResourceGroupParams.filter = $FilterType
            }
            
    
            $ResourceGroupResults = $Global:EPCWSDL_ResGrpMgmt.listResourceGroups($EPCListResourceGroupParams)
            Write-Verbose $ResourceGroupResults.result
            
            If ($ResourceGroupResults.result -eq 'success') {
                Return $ResourceGroupResults.ResourceGroupList
            }
            Else {
                Throw $ResourceGroupResults.result
            }
        }
        Else {
            [xml]$SOAPReq = "<soapenv:Envelope xmlns:soapenv='http://schemas.xmlsoap.org/soap/envelope/'
                xmlns:urn='urn:telchemyRsrcGroupMgmt'>
                 <soapenv:Header/>
                 <soapenv:Body>
                 <urn:listResourceGroupParameters>
                <credentials>
                <username>$($Global:EPControllerCred.UserName)</username>
                <password>$($Global:EPControllerCred.GetNetworkCredential().Password)</password>
                </credentials>
                 </urn:listResourceGroupParameters>
                 </soapenv:Body>
                </soapenv:Envelope>"

            
            # Add filter options if entered
            If ($ParentRGID) {
                $FilterXMLElement = $SOAPReq.Envelope.Body.listResourceGroupParameters.AppendChild($SOAPReq.CreateElement('filter'))
                $RGRelationXMLElement = $FilterXMLElement.AppendChild($SOAPReq.CreateElement('RGrelation'))
                $RGRelationshipXMLElement = $RGRelationXMLElement.AppendChild($SOAPReq.CreateElement('relationship'))
                $NULL = $RGRelationshipXMLElement.AppendChild($SOAPReq.CreateTextNode('all-children'))
                $RGIDXMLElement = $RGRelationXMLElement.AppendChild($SOAPReq.CreateElement('RGID'))
                $NULL = $RGIDXMLElement.AppendChild($SOAPReq.CreateTextNode($ParentRGID))
            }

            $SOAPFQDN = "https://$Global:EPControllerFQDN/telchemywebservices/services/telchemyRsrcGroupMgmtService"
            Write-Verbose $SOAPFQDN
            Write-Verbose $SOAPReq.OuterXML
            
            [xml]$XMLResponse = (Invoke-WebRequest -Method POST -URI $SOAPFQDN -Body $SOAPReq -ContentType 'text/xml').Content
            $ResourceGroupResults = $XMLResponse.Envelope.Body.listResourceGroupResults

            If ($ResourceGroupResults.result -eq 'success') {
                [psobject]$ResourceGroups = $ResourceGroupResults.ResourceGroupList.RG | ConvertFrom-XMLElement
                Return $ResourceGroups
            }
            Else {
                Throw $ResourceGroupResults.result
            }
        }
    }
}



#################################################################################################################################################
# #
# EPC Service Functions #
# #
#################################################################################################################################################

Function Get-EPCService {
    <#
        .SYNOPSIS
        Return list of EPC services
         
        .DESCRIPTION
        Return list of EPC services
         
        .PARAMETER ServiceClass
        Limit results to only a specific service class
         
        .EXAMPLE
        Get-EPCService -ServiceClass VOIP
        Returns service information about all VOIP services
 
        .NOTES
        Version 1.0
    #>

    
    [cmdletbinding()]
    Param (
        [Parameter(Mandatory=$False)]
        [ValidateSet('Composite','Interface','IPSec','IPTV','NetApp','NetSVC','NetTrans','Network','Other','System','TCP','VidConf','VOIP', IgnoreCase=$True)]
        [string]$ServiceClass,
        [switch]$UseXML
    )
    
    Begin {
        Connect-EPCController
        
        If ($Global:EPC_UseWSDL -And $UseXML -eq $False) {
            $EPCCred = New-Object -TypeName EPC.SvcMgmt.CredentialsType
            $EPCCred.username = $Global:EPControllerCred.UserName
            $EPCCred.password = $Global:EPControllerCred.GetNetworkCredential().Password
        }
        Else {
            $ProgressPreference = 'SilentlyContinue'
        }
    }
    Process {
        If ($Global:EPC_UseWSDL -And $UseXML -eq $False) {
            $EPCListServicesParams = New-Object -TypeName EPC.SvcMgmt.ListServicesParametersType
            $EPCListServicesParams.credentials = $EPCCred
            
            If ($ServiceClass) {
                $EPCListServicesParams.serviceClass = $ServiceClass
                $EPCListServicesParams.serviceClassSpecified = $True
            }
            
            $ServiceResults = $Global:EPCWSDL_SvcMgmt.listServices($EPCListServicesParams)
            Write-Verbose $ServiceResults.result
            
            If ($ServiceResults.result -eq 'success') {
                Return $ServiceResults.ServiceList
            }
            Else {
                Throw $ServiceResults.result
            }            
        }
        Else {
            [xml]$SOAPReq = "<soapenv:Envelope xmlns:soapenv='http://schemas.xmlsoap.org/soap/envelope/'
                xmlns:urn='urn:telchemySvcMgmt'>
                 <soapenv:Header/>
                 <soapenv:Body>
                 <urn:listServicesParameters>
                <credentials>
                <username>$($Global:EPControllerCred.UserName)</username>
                <password>$($Global:EPControllerCred.GetNetworkCredential().Password)</password>
                </credentials>
                 </urn:listServicesParameters>
                 </soapenv:Body>
                </soapenv:Envelope>"

            
            # Add the testpoint ID to the search if entered
            If ($ServiceClass) {
                $NewXMLElement = $SOAPReq.Envelope.Body.listServicesParameters.AppendChild($SOAPReq.CreateElement('serviceClass'))
                $NULL = $NewXMLElement.AppendChild($SOAPReq.CreateTextNode($ServiceClass.ToLower()))    
            }

            $SOAPFQDN = "https://$Global:EPControllerFQDN/telchemywebservices/services/telchemySvcMgmtService"
            
            [xml]$XMLResponse = (Invoke-WebRequest -Method POST -URI $SOAPFQDN -Body $SOAPReq -ContentType 'text/xml').Content
            $ServiceResults = $XMLResponse.Envelope.Body.listServicesResults
            
            If ($ServiceResults.result -eq 'success') {
                [psobject]$Services = $ServiceResults.ServiceList.service | ConvertFrom-XMLElement
                Return $Services
            }
            Else {
                Throw $ServiceResults.result
            }    
        }
    }
}


#################################################################################################################################################
# #
# EPC Test Instance Functions #
# #
#################################################################################################################################################


Function Get-EPCTestPointTestInstance {
    <#
        .SYNOPSIS
        Returns information about test point tests
         
        .DESCRIPTION
        Returns information about test point tests. Can be used to show any running tests
         
        .PARAMETER UUID
        The UUID of the test point to retrieve test information about
         
        .PARAMETER ExecMode
        Filter results by either normal or ad hoc tests
 
        .PARAMETER Status
        Filter results by the given status of a test.
         
        .EXAMPLE
        Get-EPCTestPointTestInstance -UUID d4a1437f-1f18-11ec-cf33-0daedf04882a -Status Running
        Shows running tests associated with the given test point UUID
 
        .NOTES
        Version 1.0
    #>

    
    [cmdletbinding()]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$UUID,
        [Parameter(Mandatory=$False)]
        [ValidateSet('Normal','AdHoc', IgnoreCase=$True)]
        [string]$ExecMode,
        [Parameter(Mandatory=$False)]
        [ValidateSet('Aborted','Cancelling','Cancelled','Completed','Pending','Running','Unknown', IgnoreCase=$True)]
        [string]$Status,
        [switch]$UseXML
    )
    
    Begin {
        Connect-EPCController
        
        If ($Global:EPC_UseWSDL -And $UseXML -eq $False) {
            $EPCCred = New-Object -TypeName EPC.ActiveCtrl.CredentialsType
            $EPCCred.username = $Global:EPControllerCred.UserName
            $EPCCred.password = $Global:EPControllerCred.GetNetworkCredential().Password
        }
        Else {
            $ProgressPreference = 'SilentlyContinue'
        }
    }
    Process {
        If ($Global:EPC_UseWSDL -And $UseXML -eq $False) {
            $EPCListTestPointInstancesParams = New-Object -TypeName EPC.ActiveCtrl.ListTestPointTestInstancesParametersType
            $EPCListTestPointInstancesParams.credentials = $EPCCred
        
            If ($UUID) { $EPCListTestPointInstancesParams.TestPTID = $UUID }
            
            If ($ExecMode) {
                $EPCListTestPointInstancesParams.ExecModeFilter = $ExecMode
                $EPCListTestPointInstancesParams.ExecModeFilterSpecified = $True
            }
            
            If ($Status) {
                $EPCListTestPointInstancesParams.StatusFilter = $Status
                $EPCListTestPointInstancesParams.StatusFilterSpecified = $True            
            }

            $TestPointInstancesResults = $Global:EPCWSDL_ActiveCtrl.listTestPointTestInstances($EPCListTestPointInstancesParams)
            
            Write-Verbose $TestPointInstancesResults.result
            
            If ($TestPointInstancesResults.result -eq 'success') {
                Return $TestPointInstancesResults.TestInstanceList
            }
            Else {
                Throw $TestPointInstancesResults.result
            }        
        }
        Else {
            [xml]$SOAPReq = "<soapenv:Envelope xmlns:soapenv='http://schemas.xmlsoap.org/soap/envelope/'
                xmlns:urn='urn:telchemyActiveCtrl'>
                 <soapenv:Header/>
                 <soapenv:Body>
                 <urn:listTestPointTestInstancesParameters>
                <credentials>
                <username>$($Global:EPControllerCred.UserName)</username>
                <password>$($Global:EPControllerCred.GetNetworkCredential().Password)</password>
                </credentials>
                 </urn:listTestPointTestInstancesParameters>
                 </soapenv:Body>
                </soapenv:Envelope>"

            
            # Add the testpoint ID to the search if entered
            If ($UUID) {
                $NewXMLElement = $SOAPReq.Envelope.Body.listTestPointTestInstancesParameters.AppendChild($SOAPReq.CreateElement('testptID'))
                $NULL = $NewXMLElement.AppendChild($SOAPReq.CreateTextNode($UUID))    
            }
            
            If ($ExecMode) { # WSDL uses adhoc, XML uses ad-hoc
                If ($ExecMode -eq 'AdHoc') { 
                    $ExecModeLower = 'ad-hoc' 
                }
                Else {
                    $ExecModeLower = 'normal' 
                }
                $NewXMLElement = $SOAPReq.Envelope.Body.listTestPointTestInstancesParameters.AppendChild($SOAPReq.CreateElement('execModeFilter'))
                $NULL = $NewXMLElement.AppendChild($SOAPReq.CreateTextNode($ExecModeLower))    
            }
            
            If ($Status) {
                $NewXMLElement = $SOAPReq.Envelope.Body.listTestPointTestInstancesParameters.AppendChild($SOAPReq.CreateElement('statusFilter'))
                $NULL = $NewXMLElement.AppendChild($SOAPReq.CreateTextNode($Status.ToLower()))    
            }

            $SOAPFQDN = "https://$Global:EPControllerFQDN/telchemywebservices/services/telchemyActiveCtrlService"
            
            [xml]$XMLResponse = (Invoke-WebRequest -Method POST -URI $SOAPFQDN -Body $SOAPReq -ContentType 'text/xml').Content
            $TPTestResults = $XMLResponse.Envelope.Body.listTestPointTestInstancesResults
            
            If ($TPTestResults.result -eq 'success') {
                Return $TPTestResults.TestInstanceList.testInstance
            }
            Else {
                Throw $TPTestResults.result
            }
        }
    }
}




Function Start-EPCTestInstance {
    <#
        .SYNOPSIS
        Starts an EPC test
         
        .DESCRIPTION
        Starts an EPC test
         
        .PARAMETER SrcTestPointID
        The UUID of the originating test point
         
        .PARAMETER SrcInterface
        The name of the originating network interface
         
        .PARAMETER SrcIPAddress
        The orginating test point's IP address
         
        .PARAMETER SrcResourceGroup
        The originating test point's resource group
 
        .PARAMETER DstTestPointID
        The UUID of the target test point
 
        .PARAMETER DstInterface
        The name of the target network interface
         
        .PARAMETER DstIPAddress
        The orginating test point's IP address
         
        .PARAMETER DstResourceGroup
        The target test point's resource group
 
        .PARAMETER Service
        The service name that contains the test to run
         
        .PARAMETER TestPlan
        The test plan name of the test to run
         
        .EXAMPLE
        Start-EPCTestInstance -SrcTestPointID d4a1437f-1f18-11ec-cf33-0daedf04882a -SrcInterface Ethernet -SrcIPAddress 192.168.1.100 -DstTestPointID b4a4437f-4f48-440c-cf33-0ba0bf04882a -DstInterface Wifi -DstIPAddress 192.168.0.43 -Service Contoso -TestPlan P2P_short
        Stops the given EPC test
 
        .NOTES
        Version 1.0
    #>

    
    [cmdletbinding()]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$SrcTestPointID,
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$False)]
        [string]$DstTestPointID,
        [switch]$UseXML
    )
    DynamicParam {
        # Create the dictionary
        $RuntimeParamDict = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
        
        $ParamList = @()
        $Params = [pscustomobject][ordered]@{'ParamName' = 'SrcInterface'; 'Prefix' = 'SIF'; 'ParamRequired' = $FALSE; 'Position' = 2}
        $ParamList += $Params
        $Params = [pscustomobject][ordered]@{'ParamName' = 'SrcIPAddress'; 'Prefix' = 'SIP'; 'ParamRequired' = $FALSE; 'Position' = 3}
        $ParamList += $Params
        $Params = [pscustomobject][ordered]@{'ParamName' = 'SrcResourceGroup'; 'Prefix' = 'SRG'; 'ParamRequired' = $FALSE; 'Position' = 4}
        $ParamList += $Params
        $Params = [pscustomobject][ordered]@{'ParamName' = 'DstInterface'; 'Prefix' = 'DIF'; 'ParamRequired' = $FALSE; 'Position' = 5}
        $ParamList += $Params
        $Params = [pscustomobject][ordered]@{'ParamName' = 'DstIPAddress'; 'Prefix' = 'DIP'; 'ParamRequired' = $FALSE; 'Position' = 6}
        $ParamList += $Params
        $Params = [pscustomobject][ordered]@{'ParamName' = 'DstResourceGroup'; 'Prefix' = 'DRG'; 'ParamRequired' = $FALSE; 'Position' = 7}
        $ParamList += $Params
        $Params = [pscustomobject][ordered]@{'ParamName' = 'TestPlan'; 'Prefix' = 'TP'; 'ParamRequired' = $FALSE; 'Position' = 8}
        $ParamList += $Params
        $Params = [pscustomobject][ordered]@{'ParamName' = 'Service'; 'Prefix' = 'SRV'; 'ParamRequired' = $FALSE; 'Position' = 9}
        $ParamList += $Params
        
        # Initialize parameter list
        ForEach ($Param In $ParamList) { 
            New-DynamicParam -ParamName $Param.ParamName -Prefix $Param.Prefix -ParamRequired $Param.ParamRequired -Position $Param.Position 
        }
        
        If ($SrcTestPointID) {
            # Generate and set the ValidateSet
            $SrcTestPointIFList = Get-EPCTestPointInterface -UUID $SrcTestPointID | Where {$_.Status -Contains 'ready' -And ($_.Status -Contains 'hasroute' -Or $_.Status -Contains 'has route')}
            $ValidateSetAttrib_SIF = New-Object System.Management.Automation.ValidateSetAttribute($SrcTestPointIFList.name)
            
            If ($Global:EPC_UseWSDL -And $UseXML -eq $False) {
                $SrcTestPointIPList = Get-EPCTestPointInterface -UUID $SrcTestPointID | Where {$_.Status -Contains 'ready' -And ($_.Status -Contains 'hasroute' -Or $_.Status -Contains 'has route')} | ForEach { $_.IFDetail | Select-Object -ExpandProperty Item }
            }
            Else {
                $SrcTestPointIPList = Get-EPCTestPointInterface -UUID $SrcTestPointID | Where {$_.Status -Contains 'ready' -And ($_.Status -Contains 'hasroute' -Or $_.Status -Contains 'has route')} | ForEach { ($_.IFDetail | Select-Object IPAddress).IPAddress } 
            }    
            
            $ValidateSetAttrib_SIP = New-Object System.Management.Automation.ValidateSetAttribute($SrcTestPointIPList)

            $SrcTestPointRGList = (Get-EPCTestPoint | Where {$_.testptID -eq $SrcTestPointID} | Select-Object RGList).RGList
            If ($SrcTestPointRGList.GetType().FullName -eq 'System.Xml.XmlElement') { $SrcTestPointRGList = $SrcTestPointRGList.rg } # Account for differences in XML vs WSDL
            $ValidateSetAttrib_SRG = New-Object System.Management.Automation.ValidateSetAttribute($SrcTestPointRGList)
        }
        
        If ($DstTestPointID) {
            # Generate and set the ValidateSet
            $DstTestPointIFList = Get-EPCTestPointInterface -UUID $DstTestPointID | Where {$_.Status -Contains 'ready' -And ($_.Status -Contains 'hasroute' -Or $_.Status -Contains 'has route')}
            $ValidateSetAttrib_DIF = New-Object System.Management.Automation.ValidateSetAttribute($DstTestPointIFList.name)
                        
            If ($Global:EPC_UseWSDL -And $UseXML -eq $False) {
                $DstTestPointIPList = Get-EPCTestPointInterface -UUID $DstTestPointID | Where {$_.Status -Contains 'ready' -And ($_.Status -Contains 'hasroute' -Or $_.Status -Contains 'has route')} | ForEach { $_.IFDetail | Select-Object -ExpandProperty Item }
            }
            Else {
                $DstTestPointIPList = Get-EPCTestPointInterface -UUID $DstTestPointID | Where {$_.Status -Contains 'ready' -And ($_.Status -Contains 'hasroute' -Or $_.Status -Contains 'has route')} | ForEach { ($_.IFDetail | Select-Object IPAddress).IPAddress } 
            }    
            
            $ValidateSetAttrib_DIP = New-Object System.Management.Automation.ValidateSetAttribute($DstTestPointIPList)

            $DstTestPointRGList = (Get-EPCTestPoint | Where {$_.testptID -eq $DstTestPointID} | Select-Object RGList).RGList
            If ($DstTestPointRGList.GetType().FullName -eq 'System.Xml.XmlElement') { $DstTestPointRGList = $DstTestPointRGList.rg } # Account for differences in XML vs WSDL
            $ValidateSetAttrib_DRG = New-Object System.Management.Automation.ValidateSetAttribute($DstTestPointRGList)

            $TestPlanList = Get-EPCTestPlan
            $ValidateSetAttrib_PT = New-Object System.Management.Automation.ValidateSetAttribute($TestPlanList.name)

            $ServiceList = Get-EPCService
            $ValidateSetAttrib_SRV = New-Object System.Management.Automation.ValidateSetAttribute($ServiceList.name)
        }
        
        # Add the ValidateSet to the attributes collection
        $AttribColl_SIF.Add($ValidateSetAttrib_SIF)
        $AttribColl_SIP.Add($ValidateSetAttrib_SIP)
        $AttribColl_SRG.Add($ValidateSetAttrib_SRG)
        $AttribColl_DIF.Add($ValidateSetAttrib_DIF)
        $AttribColl_DIP.Add($ValidateSetAttrib_DIP)
        $AttribColl_DRG.Add($ValidateSetAttrib_DRG)
        $AttribColl_TP.Add($ValidateSetAttrib_PT)
        $AttribColl_SRV.Add($ValidateSetAttrib_SRV)
                
        # Create and return the dynamic parameter
        $RunTimeParam_SIF = New-Object System.Management.Automation.RuntimeDefinedParameter($ParamName_SIF, [string], $AttribColl_SIF)
        $RunTimeParam_SIP = New-Object System.Management.Automation.RuntimeDefinedParameter($ParamName_SIP, [string], $AttribColl_SIP)
        $RunTimeParam_SRG = New-Object System.Management.Automation.RuntimeDefinedParameter($ParamName_SRG, [string], $AttribColl_SRG)
        $RunTimeParam_DIF = New-Object System.Management.Automation.RuntimeDefinedParameter($ParamName_DIF, [string], $AttribColl_DIF)
        $RunTimeParam_DIP = New-Object System.Management.Automation.RuntimeDefinedParameter($ParamName_DIP, [string], $AttribColl_DIP)
        $RunTimeParam_DRG = New-Object System.Management.Automation.RuntimeDefinedParameter($ParamName_DRG, [string], $AttribColl_DRG)
        $RunTimeParam_TP = New-Object System.Management.Automation.RuntimeDefinedParameter($ParamName_TP, [string], $AttribColl_TP)
        $RunTimeParam_SRV = New-Object System.Management.Automation.RuntimeDefinedParameter($ParamName_SRV, [string], $AttribColl_SRV)
        $RuntimeParamDict.Add($ParamName_SIF, $RunTimeParam_SIF)
        $RuntimeParamDict.Add($ParamName_SIP, $RunTimeParam_SIP)
        $RuntimeParamDict.Add($ParamName_SRG, $RunTimeParam_SRG)
        $RuntimeParamDict.Add($ParamName_DIF, $RunTimeParam_DIF)
        $RuntimeParamDict.Add($ParamName_DIP, $RunTimeParam_DIP)
        $RuntimeParamDict.Add($ParamName_DRG, $RunTimeParam_DRG)
        $RuntimeParamDict.Add($ParamName_TP, $RunTimeParam_TP)
        $RuntimeParamDict.Add($ParamName_SRV, $RunTimeParam_SRV)
        
        Return $RuntimeParamDict
    }

    Begin {
        Connect-EPCController
        
        # If no parameters were entered, then bring up the UI
        If ($PSBoundParameters.Count -eq 0) {
            [bool]$Script:SubmitClicked = $False
            $Script:SrcIPAddress = $NULL
            $Script:DstIPAddress = $NULL
            
            # Check to see if the module was installed to determine the location of the gui.xaml file
            $N10Path = (Get-Module -ListAvailable -Name Nectar10 -ErrorAction:SilentlyContinue).Path
            If ($N10Path) {
                $N10Path = (Get-Item $N10Path).DirectoryName
            }
            Else {
                $N10Path = '.'
            }
            
            $inputXML = Get-Content "$N10Path\epc_starttest_gui.xaml"

            $inputXML = $inputXML -replace 'mc:Ignorable="d"','' -replace "x:N",'N' -replace '^<Win.*', '<Window'
            [void][System.Reflection.Assembly]::LoadWithPartialName('presentationframework')
            [xml]$XAML = $inputXML

            #Read XAML
            $Reader=(New-Object System.Xml.XmlNodeReader $xaml)
            Try {
                $Form=[Windows.Markup.XamlReader]::Load( $Reader )
            }
            Catch {
                Throw "Unable to parse XML, with error: $($Error[0])`n Ensure that there are NO SelectionChanged or TextChanged properties in your textboxes (PowerShell cannot process them)"
            }
             
            #===========================================================================
            # Load XAML Objects In PowerShell
            #===========================================================================
              
            $xaml.SelectNodes("//*[@Name]") | %{
                Try { Set-Variable -Name "$($_.Name)" -Value $Form.FindName($_.Name) -ErrorAction Stop }
                Catch{ Throw }
                }

            # Populate the Test Plan List
            $TestPlanList = Get-EPCTestPlan | Select-Object Name, testPlanID | Sort-Object Name
            ForEach ($TestPlan in $TestPlanList) {
                [void] $TP_DropBox.Items.Add($TestPlan.name)
            }

            # Grab the default value
            $Script:TestPlanID = $TestPlanList[0].testPlanID

            # Populate the Resource Group List
            Try {
                $RGList = Get-EPCResourceGroup | Select-Object RGName, RGID | Sort-Object RGName
                ForEach ($RG in $RGList) {
                    [void] $RG_DropBox.Items.Add($RG.RGName)
                }
            }
            Catch { # Hide the RG list dropdown if user doesn't have permission to view
                $RG_Label.Visibility = 'Hidden'
                $RG_DropBox.Visibility = 'Hidden'            
            }

            # Grab the default value
            $Script:TestPlanID = $TestPlanList[0].testPlanID

            $IPRegEx = "^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$"

            # Populate the Testpoint list
            $Script:TPList = Get-EPCTestPoint | Where {$_.status -eq 'Connected'} | Select-Object Name, testptID, RGList | Sort-Object -Property Name

            ForEach ($TestPoint in $Script:TPList) {
                [void] $SrcEndpoint_ListBox.Items.Add($TestPoint.name)
                [void] $DstEndpoint_ListBox.Items.Add($TestPoint.name)
            }

            ########################
            #Add Event Handlers
            ########################
            $TP_DropBox.add_SelectionChanged({
                # Get the selected Test Plan
                $SelectedTPIndex = $TP_DropBox.SelectedIndex
                $Script:TestPlanID = $TestPlanList[$SelectedTPIndex].testPlanID
            })

            $RG_DropBox.add_SelectionChanged({
                # Get the selected Resource Group and set the test point list
                $SelectedRGIndex = $RG_DropBox.SelectedIndex
                $Script:ResourceGroupID = $RGList[$SelectedRGIndex].RGID
                $Script:TPList = Get-EPCTestPoint -RGID $Script:ResourceGroupID | Where {$_.status -eq 'Connected'} | Select-Object Name, testptID, RGList | Sort-Object -Property Name
                $SrcEndpoint_ListBox.Items.Clear()
                $DstEndpoint_ListBox.Items.Clear()
                $SrcInterface_ListBox.Items.Clear()
                $DstInterface_ListBox.Items.Clear()
                $SrcIPAddress_ListBox.Items.Clear()
                $DstIPAddress_ListBox.Items.Clear()
                
                ForEach ($TestPoint in $Script:TPList) {
                    [void] $SrcEndpoint_ListBox.Items.Add($TestPoint.name)
                    [void] $DstEndpoint_ListBox.Items.Add($TestPoint.name)
                }
            })

            $SrcEndpoint_ListBox.add_SelectionChanged({
                # Get the selected endpoint
                $SelectedEPIndex = $SrcEndpoint_ListBox.SelectedIndex
                $Script:SrcTestPointID = $Script:TPList[$SelectedEPIndex].testPtID
                $Script:SrcResourceGroupList = $Script:TPList[$SelectedEPIndex].RGList
                
                # Ensure RGList object is a PSObject instead of XML
                If ($Global:EPC_UseWSDL -eq $False -Or $UseXML) { $Script:SrcResourceGroupList = ForEach($Item in $Script:SrcResourceGroupList.RG) { $Item } }
                
                Try {
                    # Populate interface list
                    $SrcInterface_ListBox.Items.Clear()
                    $SrcIPAddress_ListBox.Items.Clear()
                    $SrcInterface_ListBox.IsEnabled = $True

                    $Script:SrcTestPointIFList = Get-EPCTestPointInterface -UUID $Script:SrcTestPointID | Where {$_.Status -Contains 'ready' -And ($_.Status -Contains 'hasroute' -Or $_.Status -Contains 'has route') -And $_.ifID -notlike 'Teredo*'} 

                    ForEach ($Interface in $Script:SrcTestPointIFList) {
                        $SrcInterface_ListBox.Items.Add($Interface.name)
                    }

                    # Auto-select the interface if there is only one
                    If (($Script:SrcTestPointIFList | Measure-Object).Count -eq 1) {
                        $SrcInterface_ListBox.SelectedIndex = 0
                    }
                }
                Catch {
                    $SrcInterface_ListBox.Items.Clear()
                    $SrcIPAddress_ListBox.Items.Clear()
                    $SrcInterface_ListBox.Items.Add("ERROR CONNECTING TO ENDPOINT")
                    $SrcInterface_ListBox.IsEnabled = $False
                }
            })


            $SrcInterface_ListBox.add_SelectionChanged({
                # Get the selected interface
                $SelectedIFIndex = $SrcInterface_ListBox.SelectedIndex
                
                If ($SelectedIFIndex -ge 0) {
                    $Script:SrcInterfaceID = $Script:SrcTestPointIFList[$SelectedIFIndex].IFID

                    # Populate the IP address list
                    If ($Global:EPC_UseWSDL -And $UseXML -eq $False) {
                        $SrcTestPointIPList = Get-EPCTestPointInterface -UUID $Script:SrcTestPointID | Where {$_.IFID -eq $Script:SrcInterfaceID} | ForEach { $_.IFDetail } | Select-Object Item | Where {$_.Item -match $IPRegEx}
                    }
                    Else {
                        $SrcTestPointIPList = Get-EPCTestPointInterface -UUID $Script:SrcTestPointID | Where {$_.IFID -eq $Script:SrcInterfaceID} | ForEach { $_.IFDetail } | Select-Object @{Name='Item';Expression={$_.IPAddress}} | Where {$_.Item -match $IPRegEx}
                    }
                    
                    $SrcIPAddress_ListBox.Items.Clear()

                    ForEach ($IPAddress in $SrcTestPointIPList) {
                        $SrcIPAddress_ListBox.Items.Add($IPAddress.Item)
                    }

                    # Auto-select the IP address if there is only one (usual case)
                    If (($SrcTestPointIPList | Measure-Object).Count -eq 1) {
                       $SrcIPAddress_ListBox.SelectedIndex = 0
                    }
                }
            })

            $SrcIPAddress_ListBox.add_SelectionChanged({
                # Get the selected IP address
                $Script:SrcIPAddress = $SrcIPAddress_ListBox.SelectedValue
                
                If ($Script:SrcIPAddress -and $Script:DstIPAddress) { 
                    $bSubmit.IsEnabled = $True 
                }
                Else {
                    $bSubmit.IsEnabled = $False
                }
            })


            $DstEndpoint_ListBox.add_SelectionChanged({
                # Get the selected endpoint
                $SelectedEPIndex = $DstEndpoint_ListBox.SelectedIndex
                $Script:DstTestPointID = $Script:TPList[$SelectedEPIndex].testPtID
                $Script:DstResourceGroupList = $Script:TPList[$SelectedEPIndex].RGList

                # Ensure RGList object is a PSObject instead of XML
                If ($Global:EPC_UseWSDL -eq $False -Or $UseXML) { $Script:DstResourceGroupList = ForEach($Item in $Script:DstResourceGroupList.RG) { $Item } }
                

                # Populate interface list
                Try {
                    $DstInterface_ListBox.Items.Clear()
                    $DstIPAddress_ListBox.Items.Clear()
                    $DstInterface_ListBox.IsEnabled = $True

                    $Script:DstTestPointIFList = Get-EPCTestPointInterface -UUID $Script:DstTestPointID | Where {$_.Status -Contains 'ready' -And ($_.Status -Contains 'hasroute' -Or $_.Status -Contains 'has route') -And $_.ifID -notlike 'Teredo*'} 

                    ForEach ($Interface in $Script:DstTestPointIFList) {
                        $DstInterface_ListBox.Items.Add($Interface.name)
                    }

                    # Auto-select the interface if there is only one
                    If (($Script:DstTestPointIFList | Measure-Object).Count -eq 1) {
                       $DstInterface_ListBox.SelectedIndex = 0
                    }
                }
                Catch {
                    $DstInterface_ListBox.Items.Clear()
                    $DstIPAddress_ListBox.Items.Clear()
                    $DstInterface_ListBox.Items.Add("ERROR CONNECTING TO ENDPOINT")
                    $DstInterface_ListBox.IsEnabled = $False
                }
            })


            $DstInterface_ListBox.add_SelectionChanged({
                # Get the selected interface
                $SelectedIFIndex = $DstInterface_ListBox.SelectedIndex
                
                If ($SelectedIFIndex -ge 0) {    
                    $Script:DstInterfaceID = $Script:DstTestPointIFList[$SelectedIFIndex].IFID

                    #Populate the IP address list
                    If ($Global:EPC_UseWSDL -And $UseXML -eq $False) {
                        $DstTestPointIPList = Get-EPCTestPointInterface -UUID $Script:DstTestPointID | Where {$_.IFID -eq $Script:DstInterfaceID} | ForEach { $_.IFDetail } | Select-Object Item | Where {$_.Item -match $IPRegEx}
                    }
                    Else {
                        $DstTestPointIPList = Get-EPCTestPointInterface -UUID $Script:DstTestPointID | Where {$_.IFID -eq $Script:DstInterfaceID} | ForEach { $_.IFDetail } | Select-Object @{Name='Item';Expression={$_.IPAddress}} | Where {$_.Item -match $IPRegEx}
                    }
                    
                    $DstIPAddress_ListBox.Items.Clear()

                    ForEach ($IPAddress in $DstTestPointIPList) {
                        $DstIPAddress_ListBox.Items.Add($IPAddress.Item)
                    }

                    # Auto-select the IP address if there is only one (usual case)
                    If (($DstTestPointIPList | Measure-Object).Count -eq 1) {
                       $DstIPAddress_ListBox.SelectedIndex = 0
                    }
                }
            })


            $DstIPAddress_ListBox.add_SelectionChanged({
                # Get the selected IP address
                $Script:DstIPAddress = $DstIPAddress_ListBox.SelectedValue

                If ($Script:DstIPAddress -and $Script:SrcIPAddress) { 
                    $bSubmit.IsEnabled = $True 
                    $bSubmit.Background = 'PaleGreen'
                }
                Else {
                    $bSubmit.IsEnabled = $False
                }
            })


            $bSubmit.Add_Click({
                [bool]$Script:SubmitClicked = $True
                $form.Close()
            })

            #Show the Form
            $form.ShowDialog() | Out-Null

            If ($Script:SubmitClicked) {
                $SrcTestPointID = $Script:SrcTestPointID
                $DstTestPointID = $Script:DstTestPointID
                
                # Set the Resource Group for source and target if it was specified
                If ($Script:ResourceGroupID) {
                    $SrcRG = $Script:ResourceGroupID
                    $DstRG = $Script:ResourceGroupID
                }
                Else { # Find a common resource group and use that for the test
                    $CommonRG = Compare-Object -IncludeEqual -ExcludeDifferent $Script:SrcResourceGroupList $Script:DstResourceGroupList
                    $SrcRG = $CommonRG[0].InputObject
                    $DstRG = $CommonRG[0].InputObject
                }
            }
            Else { Break }
        }
        
        # Write-Host "TestPlanID: $Script:TestPlanID"
        # Write-Host "SrcTPID: $Script:SrcTestPointID"
        # Write-Host "SrcIFID: $Script:SrcInterfaceID"
        # Write-Host "SrcIPID: $Script:SrcIPAddress"
        # Write-Host "DstTPID: $Script:DstTestPointID"
        # Write-Host "DstIFID: $Script:DstInterfaceID"
        # Write-Host "DstIPID: $Script:DstIPAddress"
        # Write-Host "SrcRG: $SrcRG"
        # Write-Host "DstRG: $DstRG"
        
        If ($Global:EPC_UseWSDL -And $UseXML -eq $False) {
            $EPCCred = New-Object -TypeName EPC.ActiveCtrl.CredentialsType
            $EPCCred.username = $Global:EPControllerCred.UserName
            $EPCCred.password = $Global:EPControllerCred.GetNetworkCredential().Password
        }
        Else {
            $ProgressPreference = 'SilentlyContinue'
        }
    
        If ($PSBoundParameters.Count -gt 0) {
            # Get the array position of the selected environment
            $SrcIFPos = $SrcTestPointIFList.Name.IndexOf($PsBoundParameters[$ParamName_SIF])
            $DstIFPos = $DstTestPointIFList.Name.IndexOf($PsBoundParameters[$ParamName_DIF])
            $TPPos = $TestPlanList.Name.IndexOf($PsBoundParameters[$ParamName_TP])
            $SRVPos = $ServiceList.Name.IndexOf($PsBoundParameters[$ParamName_SRV])
            
            $SrcInterfaceID = $SrcTestPointIFList[$SrcIFPos].IFID
            Write-Verbose "Src InterfaceID: $SrcInterfaceID"
            $SrcIPAddress = $PsBoundParameters[$ParamName_SIP]
            Write-Verbose "Src IPAddress: $SrcIPAddress"
            $SrcRG = $PsBoundParameters[$ParamName_SRG]
            Write-Verbose "Src RG: $SrcRG"            
            $DstInterfaceID = $DstTestPointIFList[$DstIFPos].IFID
            Write-Verbose "Dst InterfaceID: $DstInterfaceID"
            $DstIPAddress = $PsBoundParameters[$ParamName_DIP]
            Write-Verbose "Dst IPAddress: $DstIPAddress"
            $DstRG = $PsBoundParameters[$ParamName_DRG]
            Write-Verbose "Dst RG: $DstRG"
            $TestPlanID = $TestPlanList[$TPPos].TestPlanID
            Write-Verbose "TestPlanID: $TestPlanID"
            $ServiceID = $ServiceList[$SRVPos].ServiceID
            Write-Verbose "ServiceID: $ServiceID"
        }
    }
    Process {
        If ($Global:EPC_UseWSDL -And $UseXML -eq $False) {        
            $EPCSource = New-Object -TypeName EPC.ActiveCtrl.TestPointTargetType
            $EPCTarget = New-Object -TypeName EPC.ActiveCtrl.TestPointTargetType
            
            $EPCSource.TestPointID = $SrcTestPointID
            $EPCSource.ifID = $SrcInterfaceID
            $EPCSource.IPAddress = $SrcIPAddress
            $EPCSource.RG = $SrcRG

            $EPCTarget.TestPointID = $DstTestPointID
            $EPCTarget.ifID = $DstInterfaceID
            $EPCTarget.IPAddress = $DstIPAddress
            $EPCTarget.RG = $DstRG

            $EPCStartTestPlanTPtoTPParams = New-Object -TypeName EPC.ActiveCtrl.startTestPlanParamsTPtoTPType
            $EPCStartTestPlanTPtoTPParams.TestPlanID = $TestPlanID
            $EPCStartTestPlanTPtoTPParams.Originator = $EPCSource
            $EPCStartTestPlanTPtoTPParams.TPTarget = $EPCTarget
            
            If ($Service) { Write-Verbose "Selected Service: $ServiceID"; $EPCStartTestPlanTPtoTPParams.ServiceID = $ServiceID }    
            
            $EPCStartTestPlanParams = New-Object -TypeName EPC.ActiveCtrl.startTestPlanParametersType
            $EPCStartTestPlanParams.credentials = $EPCCred
            $EPCStartTestPlanParams.Items = $EPCStartTestPlanTPtoTPParams
            $EPCStartTestPlanParams.ItemsElementName = 'tp2tptest'
            
            $StartTestPlanResults = $Global:EPCWSDL_ActiveCtrl.startTestPlan($EPCStartTestPlanParams)
            Write-Verbose $StartTestPlanResults.result
        }
        Else {
            [xml]$SOAPReq = "<soapenv:Envelope xmlns:soapenv='http://schemas.xmlsoap.org/soap/envelope/'
                xmlns:urn='urn:telchemyActiveCtrl'>
                 <soapenv:Header/>
                 <soapenv:Body>
                 <urn:startTestPlanParameters>
                 <credentials>
                 <username>$($Global:EPControllerCred.UserName)</username>
                 <password>$($Global:EPControllerCred.GetNetworkCredential().Password)</password>
                 </credentials>
                 <tp2tpTest>
                 <testplanID>$TestPlanID</testplanID>
                 <originator>
                 <testpointID>$SrcTestPointID</testpointID>
                 <ifID>$SrcInterfaceID</ifID>
                 <ipAddress>$SrcIPAddress</ipAddress>
                 <RG>$SrcRG</RG>
                 </originator>
                 <tpTarget>
                 <testpointID>$DstTestPointID</testpointID>
                 <ifID>$DstInterfaceID</ifID>
                 <ipAddress>$DstIPAddress</ipAddress>
                 <RG>$DstRG</RG>
                 </tpTarget>
                 </tp2tpTest>
                 </urn:startTestPlanParameters>
                 </soapenv:Body>
                </soapenv:Envelope>"

                
            Write-Verbose $SOAPReq.OuterXML

            $SOAPFQDN = "https://$Global:EPControllerFQDN/telchemywebservices/services/telchemyActiveCtrlService"
            
            Write-Verbose $SOAPFQDN
            
            [xml]$XMLResponse = (Invoke-WebRequest -Method POST -URI $SOAPFQDN -Body $SOAPReq -ContentType 'text/xml').Content
            $StartTestPlanResults = $XMLResponse.Envelope.Body.StartTestPlanResults
        }
        
        If ($StartTestPlanResults.result -eq 'success') {
            Return "Successfully started test`n`rInstance ID: $($StartTestPlanResults.instanceID)"
        }
        Else {
            Return $StartTestPlanResults.result
        }
    }
}



Function Stop-EPCTestInstance {
    <#
        .SYNOPSIS
        Stops a running EPC test
         
        .DESCRIPTION
        Stops a running EPC test
         
        .PARAMETER InstanceID
        The InstanceID of the test to stop
         
        .EXAMPLE
        Stop-EPCTestInstance -InstanceID MDNkMGQxZGQ4YzVjY2VlYmRkZTBlYjA2MWRlLTg3MjI=<
        Stops the given EPC test
 
        .NOTES
        Version 1.0
    #>

    
    [cmdletbinding()]
    Param (
        [Parameter(ValueFromPipelineByPropertyName, Mandatory=$True)]
        [string]$InstanceID,
        [switch]$UseXML
    )
    
    Begin {
        Connect-EPCController
        
        If ($Global:EPC_UseWSDL -And $UseXML -eq $False) {
            $EPCCred = New-Object -TypeName EPC.ActiveCtrl.CredentialsType
            $EPCCred.username = $Global:EPControllerCred.UserName
            $EPCCred.password = $Global:EPControllerCred.GetNetworkCredential().Password
        }
        Else {
            $ProgressPreference = 'SilentlyContinue'
        }
    }
    Process {
        If ($Global:EPC_UseWSDL -And $UseXML -eq $False) {
            $EPCStopTestPlanParams = New-Object -TypeName EPC.ActiveCtrl.StopTestPlanParametersType
            $EPCStopTestPlanParams.credentials = $EPCCred
            $EPCStopTestPlanParams.instanceID = $InstanceID

            $StopTestPlanResults = $Global:EPCWSDL_ActiveCtrl.stopTestPlan($EPCStopTestPlanParams)
        }
        Else {
            [xml]$SOAPReq = "<soapenv:Envelope xmlns:soapenv='http://schemas.xmlsoap.org/soap/envelope/'
                xmlns:urn='urn:telchemyActiveCtrl'>
                 <soapenv:Header/>
                 <soapenv:Body>
                 <urn:stopTestPlanParameters>
                <credentials>
                 <username>$($Global:EPControllerCred.UserName)</username>
                 <password>$($Global:EPControllerCred.GetNetworkCredential().Password)</password>
                </credentials>
                 <instanceID>$InstanceID</instanceID>
                 </urn:stopTestPlanParameters>
                 </soapenv:Body>
                </soapenv:Envelope>"

                
            Write-Verbose $SOAPReq.OuterXML

            $SOAPFQDN = "https://$Global:EPControllerFQDN/telchemywebservices/services/telchemyActiveCtrlService"
            
            Write-Verbose $SOAPFQDN
            
            [xml]$XMLResponse = (Invoke-WebRequest -Method POST -URI $SOAPFQDN -Body $SOAPReq -ContentType 'text/xml').Content
            $StopTestPlanResults = $XMLResponse.Envelope.Body.stopTestPlanResults
        }
        
        If ($StopTestPlanResult.result -eq 'success') { 
            Return 'Test stopped successfully'
        }
        Else {
            Throw $StopTestPlanResult.result
        }
    }
}







#################################################################################################################################################
# #
# Supporting Functions #
# #
#################################################################################################################################################

Function Convert-NectarNumToTelURI {
    <#
        .SYNOPSIS
        Converts a Nectar formatted number "+12223334444 x200" into a valid TEL uri "+12223334444;ext=200"
 
        .DESCRIPTION
        Converts a Nectar formatted number "+12223334444 x200" into a valid TEL uri "+12223334444;ext=200"
         
        .PARAMETER PhoneNumber
        The phone number to convert to a TEL URI
 
        .PARAMETER TenantName
        The name of the Nectar 10 tenant. Used in multi-tenant configurations.
         
        .EXAMPLE
        Convert-NectarNumToTelsURI "+12224243344 x3344"
        Converts the above number to a TEL URI
         
        .EXAMPLE
        Get-NectarUnallocatedNumber -LocationName Jericho | Convert-NectarNumToTelURI
        Returns the next available phone number in the Jericho location in Tel URI format
             
        .NOTES
        Version 1.1
    #>

    
    Param (
        [Parameter(ValueFromPipeline,ValueFromPipelineByPropertyName, Mandatory=$true)]
        [Alias("number")]
        [string]$PhoneNumber
    )
    
    $PhoneNumber = "tel:" + $PhoneNumber.Replace(" x", ";ext=")
    Return $PhoneNumber    
}



Function Get-NectarDefaultTenantName {
    <#
        .SYNOPSIS
        Set the default tenant name for use with other commands.
         
        .DESCRIPTION
        Set the default tenant name for use with other commands.
 
        .NOTES
        Version 1.0
    #>

    
    [Alias("stne")]
    Param ()

    If ($Global:NectarTenantName) { # Use globally set tenant name, if one was set and not explicitly included in the command
        Return $Global:NectarTenantName 
    }
    ElseIf (!$TenantName -And !$Global:NectarTenantName) { # If a tenant name wasn't set (normal for most connections, set the TenantName variable if only one available
        $TenantList = Invoke-RestMethod -Method GET -Credential $Global:NectarCred -uri "https://$Global:NectarCloud/aapi/tenant"
        If ($TenantList.Count -eq 1) {
            Return $TenantList
        }
        Else {
            $TenantList | %{$TList += ($(if($TList){", "}) + $_)}
            Write-Error "TenantName was not specified. Select one of $TList"
            $PSCmdlet.ThrowTerminatingError()
            Return 
        }
    }
}



Function Get-LatLong {
    <#
        .SYNOPSIS
        Returns the geographical coordinates for an address.
 
        .DESCRIPTION
        Returns the geographical coordinates for an address.
         
        .PARAMETER Address
        The address of the location to return information on. Include as much detail as possible.
             
        .EXAMPLE
        Get-LatLong -Address "33 Main Street, Jonestown, NY, USA"
        Retrieves the latitude/longitude for the selected location
         
        .NOTES
        Version 1.0
    #>

    
    Param(
        [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
        [String]$Address
    )

    Begin {
        $GoogleGeoAPIKey = [System.Environment]::GetEnvironmentVariable('GoogleGeocode_API_Key','user')
        If(!$GoogleGeoAPIKey) {
            Write-Host -ForegroundColor Red "You need to register for an API key and save it as persistent environment variable called GoogleGeocode_API_Key on this machine. Follow this link to get an API Key - https://developers.google.com/maps/documentation/geocoding/get-api-key "
            $GoogleGeoAPIKey = Read-Host "Enter a valid Google API key to be saved as environment variable GoogleGeocode_API_Key"
            [System.Environment]::SetEnvironmentVariable('GoogleGeocode_API_Key', $GoogleGeoAPIKey,[System.EnvironmentVariableTarget]::User)
        }
    }
    Process {
        Try    {
            $JSON = Invoke-RestMethod -Method GET -Uri "https://maps.googleapis.com/maps/api/geocode/json?address=$Address&key=$GoogleGeoAPIKey" -ErrorVariable EV
            $Status = $JSON.Status
            
            [double]$Lat = $JSON.results.geometry.location.lat
            [double]$Lng = $JSON.results.geometry.location.lng
            
            $LatLong = New-Object PSObject
            
            If($Status -eq "OK") {
                $LatLong | Add-Member -type NoteProperty -Name 'Latitude' -Value $Lat
                $LatLong | Add-Member -type NoteProperty -Name 'Longitude' -Value $Lng
            }
            ElseIf ($Status -eq 'ZERO_RESULTS') {
                $LatLong | Add-Member -type NoteProperty -Name 'Latitude' -Value 0
                $LatLong | Add-Member -type NoteProperty -Name 'Longitude' -Value 0                
            }
            Else {
                $ErrorMessage = $JSON.error_message
                Write-Host -ForegroundColor Yellow "WARNING: Address geolocation failed for the following reason: $Status - $ErrorMessage"
                $LatLong | Add-Member -type NoteProperty -Name 'Latitude' -Value 0
                $LatLong | Add-Member -type NoteProperty -Name 'Longitude' -Value 0
            }
        }
        Catch {
            "Something went wrong. Please try again."
            $ev.message
        }
        Return $LatLong
    }
}



Function New-DynamicParam {
    [cmdletbinding()]
    Param (
        [Parameter(Mandatory=$True)]
        [string]$ParamName,
        [Parameter(Mandatory=$True)]
        [string]$Prefix,
        [Parameter(Mandatory=$True)]
        [bool]$ParamRequired,
        [Parameter(Mandatory=$True)]
        [int]$Position        
    )
    
    Process {
        # Set the dynamic parameters' name
        New-Variable -Name "ParamName_$Prefix" -Value $ParamName -Scope Script -Force
        
        # Create the collection of attributes
        $AttribColl_XXX = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
        
        # Create and set the parameters' attributes
        $ParamAttrib_XXX = New-Object System.Management.Automation.ParameterAttribute
        $ParamAttrib_XXX.Mandatory = $ParamRequired
        $ParamAttrib_XXX.Position = $Position
                
        # Add the attributes to the attributes collection
        $AttribColl_XXX.Add($ParamAttrib_XXX)
        
        New-Variable -Name "AttribColl_$Prefix" -Value $AttribColl_XXX -Scope Script -Force
    }
}


Function ConvertFrom-XMLElement {
    <#
    .Synopsis
        Converts named nodes of an element to properties of a PSObject, recursively.
 
    .Parameter Element
        The element to convert to a PSObject.
 
    .Parameter SelectXmlInfo
        Output from the Select-Xml cmdlet.
 
    .Inputs
        Microsoft.PowerShell.Commands.SelectXmlInfo output from Select-Xml.
 
    .Outputs
        System.Management.Automation.PSCustomObject object created from selected XML.
 
    .Link
        Select-Xml
 
    .Example
        Select-Xml /configuration/appSettings/add web.config |ConvertFrom-XmlElement
 
        key value
        --- -----
        webPages:Enabled false
    #>


    #Requires -Version 3
    [CmdletBinding()][OutputType([psobject])] Param(
    [Parameter(ParameterSetName='Element',Position=0,Mandatory=$true,ValueFromPipeline=$true)][Xml.XmlElement] $Element,
    [Parameter(ParameterSetName='SelectXmlInfo',Position=0,Mandatory=$true,ValueFromPipeline=$true)]
    [Microsoft.PowerShell.Commands.SelectXmlInfo]$SelectXmlInfo
    )
    Process
    {
        switch($PSCmdlet.ParameterSetName)
        {
            SelectXmlInfo { @($SelectXmlInfo |% {[Xml.XmlElement]$_.Node} | ConvertFrom-XmlElement) }
            Element
            {
                if(($Element.SelectNodes('*') |group Name |measure).Count -eq 1)
                {
                    @($Element.SelectNodes('*') |ConvertFrom-XmlElement)
                }
                else
                {
                    $properties = @{}
                    $Element.Attributes |% {[void]$properties.Add($_.Name,$_.Value)}
                    foreach($node in $Element.ChildNodes |? {$_.Name -and $_.Name -ne '#whitespace'})
                    {
                        $subelements = $node.SelectNodes('*') |group Name
                        $value =
                            if($node.InnerText -and !$subelements)
                            {
                                $node.InnerText
                            }
                            elseif(($subelements |measure).Count -eq 1)
                            {
                                @($node.SelectNodes('*') | ConvertFrom-XmlElement)
                            }
                            else
                            {
                                ConvertFrom-XmlElement $node
                            }
                        if(!$properties.Contains($node.Name))
                        { # new property
                            [void]$properties.Add($node.Name,$value)
                        }
                        else
                        { # property name collision!
                            if($properties[$node.Name] -isnot [Collections.Generic.List[object]])
                            { $properties[$node.Name] = ([Collections.Generic.List[object]]@($properties[$node.Name],$value)) }
                            else
                            { $properties[$node.Name].Add($value) }
                        }
                    }
                    New-Object PSObject -Property $properties
                }
            }
        }
    }    
}


Function New-EPCTestPointXML {
    [cmdletbinding()]
    Param (
        [Parameter(Mandatory=$True)]
        $Data        
    )

    $xmlData = '<testPointList>'
    ForEach ($Obj in $Data) {    
        $Properties = $Obj |  Get-Member -MemberType NoteProperty | Select-Object -ExpandProperty Name
        $xmlData += '<testPoint>'
        foreach ($Property in $Properties) {
            $xmlData += "<$Property>$($Obj.$Property)</$Property>"
        }
        $xmlData += '</testPoint>'
    }
    $xmlData += '</testPointList>'
    Return $xmlData    
}



Function Show-GroupAndStats {
    <#
        .SYNOPSIS
        Groups a set of data by one parameter, and shows sum, average, min, max for another numeric parameter (such as duration)
 
        .DESCRIPTION
        Groups a set of data by one parameter, and shows sum, average, min, max for another numeric parameter (such as duration)
         
        .PARAMETER Input
        The data to group and sum. Can be pipelined
 
        .PARAMETER GroupBy
        The parameter to group on
         
        .PARAMETER SumBy
        The parameter to calculate numerical statistics. Must be a numeric field
 
        .PARAMETER ShowSumByAsTimeFormat
        If the SumBy parameter is in seconds (Duration is an example), format the output as dd.hh:mm:ss instead of seconds
         
        .EXAMPLE
        Get-NectarSessions | Show-GroupAndStats -GroupBy CallerLocation -SumBy Duration
        Will group all calls by caller location and show sum, avg, min, max for the Duration column
             
        .NOTES
        Version 1.0
    #>

    
    Param (
        [Parameter(ValueFromPipeline, Mandatory=$True)]
        [pscustomobject[]]$Input,
        [Parameter(Mandatory=$True)]
        [string]$GroupBy,
        [Parameter(Mandatory=$True)]
        [string]$SumBy,
        [switch]$ShowSumByAsTimeFormat
    )    

    # Validate the parameters exist and are the proper format
    If (($Input | Get-Member $GroupBy) -eq $NULL) {
        Write-Error "$GroupBy is not a valid parameter for the source data"
        Break
    }
    
    If (($Input | Get-Member $SumBy) -eq $NULL) {
        Write-Error "$SumBy is not a valid parameter for the source data"
        Break
    }
    ElseIf (($Input | Get-Member $SumBy).Definition -NotMatch 'int|float|double') {
        Write-Error "$SumBy is not a numeric field"
        Break    
    }
        
    
    Try {
        If ($ShowSumByAsTimeFormat) {
            $Input | Group-Object $GroupBy | Sort-Object Count -Descending | %{
                [pscustomobject][ordered] @{
                    $GroupBy = $_.Name
                    'Count' = $_.Count
                    "SUM_$SumBy" = [timespan]::FromSeconds(($_.Group | Measure-Object $SumBy -Sum).Sum)
                    "AVG_$SumBy" = [timespan]::FromSeconds([math]::Round(($_.Group | Measure-Object $SumBy -Average).Average))
                    "MIN_$SumBy" = [timespan]::FromSeconds(($_.Group | Measure-Object $SumBy -Minimum).Minimum)
                    "MAX_$SumBy" = [timespan]::FromSeconds(($_.Group | Measure-Object $SumBy -Maximum).Maximum)
                }
            }             
        }
        Else {
            $Input | Group-Object $GroupBy | Sort-Object Count -Descending | %{
                [pscustomobject][ordered] @{
                    $GroupBy = $_.Name
                    'Count' = $_.Count
                    "SUM_$SumBy" = ($_.Group | Measure-Object $SumBy -Sum).Sum
                    "AVG_$SumBy" = [math]::Round(($_.Group | Measure-Object $SumBy -Average).Average,2)
                    "MIN_$SumBy" = ($_.Group | Measure-Object $SumBy -Minimum).Minimum
                    "MAX_$SumBy" = ($_.Group | Measure-Object $SumBy -Maximum).Maximum
                }
            } 
        }
    }
    Catch {
        Write-Error "Error showing output."
    }
}



Function Get-JSONErrorStream {
    <#
        .SYNOPSIS
        Returns the error text of a JSON stream
 
        .DESCRIPTION
        Returns the error text of a JSON stream
         
        .PARAMETER JSONResponse
        The error response
             
        .EXAMPLE
        Get-JSONErrorStream $_
        Returns the error message from a JSON stream that errored out.
         
        .NOTES
        Version 1.0
    #>

    Param(
        [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
        $JSONResponse
    )

    $ResponseStream = $JSONResponse.Exception.Response.GetResponseStream()
    $Reader = New-Object System.IO.StreamReader($ResponseStream)
    $ResponseBody = $Reader.ReadToEnd() | ConvertFrom-Json
    Write-Host -ForegroundColor Red -BackgroundColor Black $ResponseBody.errorMessage
    Write-Host
    Write-Host
}


Function New-JWT {
    <#
        .SYNOPSIS
        Generates a JSON Web Ticket (JWT) for authenticating with services like Zoom
 
        .DESCRIPTION
        Generates a JSON Web Ticket (JWT) for authenticating with services like Zoom
         
        .PARAMETER JSONResponse
        The error response
             
        .EXAMPLE
        Get-JSONErrorStream $_
        Returns the error message from a JSON stream that errored out.
         
        .NOTES
        Version 1.0
    #>

    Param(
        [Parameter(Mandatory = $True)]
        [ValidateSet("HS256", "HS384", "HS512")]
        $Algorithm = $null,
        $type = $null,
        [Parameter(Mandatory = $True)]
        [string]$Issuer = $null,
        [int]$ValidforSeconds = $null,
        [Parameter(Mandatory = $True)]
        $SecretKey = $null
    )

    $exp = [int][double]::parse((Get-Date -Date $((Get-Date).addseconds($ValidforSeconds).ToUniversalTime()) -UFormat %s)) # Grab Unix Epoch Timestamp and add desired expiration.

    [hashtable]$header = @{alg = $Algorithm; typ = $type}
    [hashtable]$payload = @{iss = $Issuer; exp = $exp}

    $headerjson = $header | ConvertTo-Json -Compress
    $payloadjson = $payload | ConvertTo-Json -Compress
    
    $headerjsonbase64 = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($headerjson)).Split('=')[0].Replace('+', '-').Replace('/', '_')
    $payloadjsonbase64 = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($payloadjson)).Split('=')[0].Replace('+', '-').Replace('/', '_')

    $ToBeSigned = $headerjsonbase64 + "." + $payloadjsonbase64

    $SigningAlgorithm = switch ($Algorithm) {
        "HS256" {New-Object System.Security.Cryptography.HMACSHA256}
        "HS384" {New-Object System.Security.Cryptography.HMACSHA384}
        "HS512" {New-Object System.Security.Cryptography.HMACSHA512}
    }

    $SigningAlgorithm.Key = [System.Text.Encoding]::UTF8.GetBytes($SecretKey)
    $Signature = [Convert]::ToBase64String($SigningAlgorithm.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($ToBeSigned))).Split('=')[0].Replace('+', '-').Replace('/', '_')
    
    $token = "$headerjsonbase64.$payloadjsonbase64.$Signature"
    $token
}


Function ParseBool {
    [CmdletBinding()]
    param(
        [Parameter(Position=0)]
        [System.String]$inputVal
    )
    switch -regex ($inputVal.Trim()) {
        "^(1|true|yes|on|enabled)$" { Return $True }
        default { Return $False }
    }
}


Export-ModuleMember -Alias * -Function *




# SIG # Begin signature block
# MIINEQYJKoZIhvcNAQcCoIINAjCCDP4CAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB
# gjcCAQSgWzBZMDQGCisGAQQBgjcCAR4wJgIDAQAABBAfzDtgWUsITrck0sYpfvNR
# AgEAAgEAAgEAAgEAAgEAMCEwCQYFKw4DAhoFAAQUw/ro9BvziYVzL+mSk09I2gdC
# brygggpTMIIFGzCCBAOgAwIBAgIQCVcmswfJ1koJSkb+PKEcYzANBgkqhkiG9w0B
# AQsFADByMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYD
# VQQLExB3d3cuZGlnaWNlcnQuY29tMTEwLwYDVQQDEyhEaWdpQ2VydCBTSEEyIEFz
# c3VyZWQgSUQgQ29kZSBTaWduaW5nIENBMB4XDTIwMDIxMzAwMDAwMFoXDTIzMDIx
# NzEyMDAwMFowWDELMAkGA1UEBhMCQ0ExEDAOBgNVBAgTB09udGFyaW8xDzANBgNV
# BAcTBkd1ZWxwaDESMBAGA1UEChMJS2VuIExhc2tvMRIwEAYDVQQDEwlLZW4gTGFz
# a28wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCsCGnJWjBqNX+R8Pyv
# 24IX7EnQkYm8i/VOd5dpxUMKOdnq+oEi2fr+tkWHtSgggbjTdcXP4l6fBxBzuGB2
# q12eLaa136Um4KAmYRnuqJ2IXfdEyW8/Zib7FVzUV41dwRBVH/VF+QZOHxwcL0MJ
# 5OwiRSLiMWYqWk7c+8UIFpDe17Pjevy8g2o0RcTAhyDeEZ1FPAIFk/nkirB5psMz
# mC5TfCKkuxQWOg3/F78KnvBxuVl7q9QcS2BeJXrospvQ130qRMOjrcO6suuRjtrT
# iuMt3CjKtStnqKAY/2yPV1Gvlg4itoO1quANvoNgYB66B3zQZMBGicdwnq0nkG7B
# vPENAgMBAAGjggHFMIIBwTAfBgNVHSMEGDAWgBRaxLl7KgqjpepxA8Bg+S32ZXUO
# WDAdBgNVHQ4EFgQUHAoKnWsI9RGj62kGJANJvR36UXYwDgYDVR0PAQH/BAQDAgeA
# MBMGA1UdJQQMMAoGCCsGAQUFBwMDMHcGA1UdHwRwMG4wNaAzoDGGL2h0dHA6Ly9j
# cmwzLmRpZ2ljZXJ0LmNvbS9zaGEyLWFzc3VyZWQtY3MtZzEuY3JsMDWgM6Axhi9o
# dHRwOi8vY3JsNC5kaWdpY2VydC5jb20vc2hhMi1hc3N1cmVkLWNzLWcxLmNybDBM
# BgNVHSAERTBDMDcGCWCGSAGG/WwDATAqMCgGCCsGAQUFBwIBFhxodHRwczovL3d3
# dy5kaWdpY2VydC5jb20vQ1BTMAgGBmeBDAEEATCBhAYIKwYBBQUHAQEEeDB2MCQG
# CCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wTgYIKwYBBQUHMAKG
# Qmh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFNIQTJBc3N1cmVk
# SURDb2RlU2lnbmluZ0NBLmNydDAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUA
# A4IBAQAVlWAZaJ+IYUINcKhTdojQ3ViHdfcuzgTMECn1bCbr4DxH/SDmNr1n3Nm9
# ZIodfPE5fjFuqaYqQvfzFutqBTiX5hStT284WpGjrPwSuiJDRoI3iBTjyy5OABpi
# kpgdncJeyNvEO2ermcBrVw4t4AUZKfYsyjxfXaX8INcvHdNGhTiN5x4SjGSXxSvx
# hr7F9aLeE0mG+5yDlr5HbfPbyqLWdvLP4UcQ9WrJOmN0wa7qanrErr3ZeuDZQebL
# zEesJy1VCY2bqTEI8/fyTqnlLjut7i9dvp84zKomX30lqy9R81WUas9XruMLfgVR
# 3BVuBoyVtdx4AmgVzHoznDWs/vh/MIIFMDCCBBigAwIBAgIQBAkYG1/Vu2Z1U0O1
# b5VQCDANBgkqhkiG9w0BAQsFADBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGln
# aUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtE
# aWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0EwHhcNMTMxMDIyMTIwMDAwWhcNMjgx
# MDIyMTIwMDAwWjByMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5j
# MRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMTEwLwYDVQQDEyhEaWdpQ2VydCBT
# SEEyIEFzc3VyZWQgSUQgQ29kZSBTaWduaW5nIENBMIIBIjANBgkqhkiG9w0BAQEF
# AAOCAQ8AMIIBCgKCAQEA+NOzHH8OEa9ndwfTCzFJGc/Q+0WZsTrbRPV/5aid2zLX
# cep2nQUut4/6kkPApfmJ1DcZ17aq8JyGpdglrA55KDp+6dFn08b7KSfH03sjlOSR
# I5aQd4L5oYQjZhJUM1B0sSgmuyRpwsJS8hRniolF1C2ho+mILCCVrhxKhwjfDPXi
# TWAYvqrEsq5wMWYzcT6scKKrzn/pfMuSoeU7MRzP6vIK5Fe7SrXpdOYr/mzLfnQ5
# Ng2Q7+S1TqSp6moKq4TzrGdOtcT3jNEgJSPrCGQ+UpbB8g8S9MWOD8Gi6CxR93O8
# vYWxYoNzQYIH5DiLanMg0A9kczyen6Yzqf0Z3yWT0QIDAQABo4IBzTCCAckwEgYD
# VR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAYYwEwYDVR0lBAwwCgYIKwYB
# BQUHAwMweQYIKwYBBQUHAQEEbTBrMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5k
# aWdpY2VydC5jb20wQwYIKwYBBQUHMAKGN2h0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0
# LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcnQwgYEGA1UdHwR6MHgwOqA4
# oDaGNGh0dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJv
# b3RDQS5jcmwwOqA4oDaGNGh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2Vy
# dEFzc3VyZWRJRFJvb3RDQS5jcmwwTwYDVR0gBEgwRjA4BgpghkgBhv1sAAIEMCow
# KAYIKwYBBQUHAgEWHGh0dHBzOi8vd3d3LmRpZ2ljZXJ0LmNvbS9DUFMwCgYIYIZI
# AYb9bAMwHQYDVR0OBBYEFFrEuXsqCqOl6nEDwGD5LfZldQ5YMB8GA1UdIwQYMBaA
# FEXroq/0ksuCMS1Ri6enIZ3zbcgPMA0GCSqGSIb3DQEBCwUAA4IBAQA+7A1aJLPz
# ItEVyCx8JSl2qB1dHC06GsTvMGHXfgtg/cM9D8Svi/3vKt8gVTew4fbRknUPUbRu
# pY5a4l4kgU4QpO4/cY5jDhNLrddfRHnzNhQGivecRk5c/5CxGwcOkRX7uq+1UcKN
# JK4kxscnKqEpKBo6cSgCPC6Ro8AlEeKcFEehemhor5unXCBc2XGxDI+7qPjFEmif
# z0DLQESlE/DmZAwlCEIysjaKJAL+L3J+HNdJRZboWR3p+nRka7LrZkPas7CM1ekN
# 3fYBIM6ZMWM9CBoYs4GbT8aTEAb8B4H6i9r5gkn3Ym6hU/oSlBiFLpKR6mhsRDKy
# ZqHnGKSaZFHvMYICKDCCAiQCAQEwgYYwcjELMAkGA1UEBhMCVVMxFTATBgNVBAoT
# DERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTExMC8GA1UE
# AxMoRGlnaUNlcnQgU0hBMiBBc3N1cmVkIElEIENvZGUgU2lnbmluZyBDQQIQCVcm
# swfJ1koJSkb+PKEcYzAJBgUrDgMCGgUAoHgwGAYKKwYBBAGCNwIBDDEKMAigAoAA
# oQKAADAZBgkqhkiG9w0BCQMxDAYKKwYBBAGCNwIBBDAcBgorBgEEAYI3AgELMQ4w
# DAYKKwYBBAGCNwIBFTAjBgkqhkiG9w0BCQQxFgQUkkCkQ++dAuzY/ScTjRMEjQUX
# KQ0wDQYJKoZIhvcNAQEBBQAEggEAln+6T6MOZitAQwPKrpdehsfVWUm0F9skfhEt
# 3SgcdqRAVP2ybPViFy1hX1in327/Mhg3VS+ThXcPM1D9z/nGbe4rcphIPNjn4L5k
# gMQYA0LcjsLgIKuOMspsXU4dw6XoZD+l72yCtNT0rHDxxriKqL+Snq1dnvaLq9Hj
# 6+RorRQVW128LGyBQ9SNDUC6A70DcvdSHdqEzeLWW+Z2TxsQS9qO3f2muctP68KE
# m4PCq6L0wsDULvtTPD0Nwx6T99NKmlv6EIVpPCm0tmv2nNtC3P1GvKdNXYscteZ3
# TrMpCwr6A8ccp2TA1FzGs24PBYz3un9+PgVgFst8sRqKNhv1nA==
# SIG # End signature block