functions/Get-WUGDeviceGroupReport.ps1

<#
.SYNOPSIS
    Retrieves performance and availability reports for one or more WhatsUp Gold device groups.
 
.DESCRIPTION
    Get-WUGDeviceGroupReport queries the WhatsUp Gold REST API for group-level reports across multiple
    report types such as CPU, memory, disk, interface, ping, state change, and maintenance mode.
    Results are automatically paginated. Supports hierarchy inclusion, sorting, grouping, thresholds,
    time ranges, and business hours filtering.
 
.PARAMETER GroupId
    One or more device group IDs to retrieve reports for. Accepts pipeline input and the alias 'id'. Required.
 
.PARAMETER ReportType
    The type of report to retrieve. Required. Valid values: Cpu, Disk, DiskSpaceFree, Interface,
    InterfaceDiscards, InterfaceErrors, InterfaceTraffic, Memory, PingAvailability, PingResponseTime, StateChange, Maintenance.
 
.PARAMETER ReturnHierarchy
    Include devices from descendant groups. Valid values: true, false. Default from API: false (only devices in the specified group).
 
.PARAMETER Range
    The time range preset for the report. Valid values: today, lastPolled, yesterday, lastWeek, lastMonth,
    lastQuarter, weekToDate, monthToDate, quarterToDate, lastNSeconds, lastNMinutes, lastNHours, lastNDays,
    lastNWeeks, lastNMonths, custom.
 
.PARAMETER RangeStartUtc
    The start date/time in UTC for a custom time range. Used when Range is set to 'custom'.
 
.PARAMETER RangeEndUtc
    The end date/time in UTC for a custom time range. Used when Range is set to 'custom'.
 
.PARAMETER RangeN
    The number of time units for lastN* range types (e.g., lastNHours with RangeN=4 means last 4 hours). Default: 1.
 
.PARAMETER SortBy
    The column to sort results by. Valid values depend on the ReportType selected.
 
.PARAMETER SortByDir
    The sort direction. Valid values: asc, desc. Default: desc.
 
.PARAMETER GroupBy
    The column to group results by. Valid values depend on the ReportType selected.
 
.PARAMETER GroupByDir
    The group sort direction. Valid values: asc, desc.
 
.PARAMETER ApplyThreshold
    Whether to apply a threshold filter to the results. Valid values: true, false.
 
.PARAMETER OverThreshold
    When ApplyThreshold is true, determines whether to return values over or under the threshold. Valid values: true, false.
 
.PARAMETER ThresholdValue
    The numeric threshold value to filter against. Default: 0.0.
 
.PARAMETER BusinessHoursId
    The ID of a business hours profile to restrict the report to. Default: 0 (all hours).
 
.PARAMETER RollupByDevice
    Whether to roll up (aggregate) results per device instead of individual resources. Valid values: true, false.
 
.PARAMETER PageId
    The page identifier for retrieving a specific page of paginated results.
 
.PARAMETER Limit
    The maximum number of results per page. Valid range: 0-250.
 
.EXAMPLE
    Get-WUGDeviceGroupReport -GroupId 0 -ReportType Cpu -Range lastWeek
 
    Returns the CPU utilization report for the root device group over the last week.
 
.EXAMPLE
    Get-WUGDeviceGroupReport -GroupId 5 -ReportType Memory -Range lastNHours -RangeN 8 -ReturnHierarchy true
 
    Returns the memory utilization report for group 5 and all its descendant groups over the last 8 hours.
 
.EXAMPLE
    Get-WUGDeviceGroupReport -GroupId 10 -ReportType PingAvailability -Range custom -RangeStartUtc '2026-03-01T00:00:00Z' -RangeEndUtc '2026-03-06T00:00:00Z'
 
    Returns the ping availability report for group 10 within a custom UTC date range.
 
.EXAMPLE
    Get-WUGDeviceGroupReport -GroupId 3,7 -ReportType Disk -Range lastMonth -ApplyThreshold true -OverThreshold true -ThresholdValue 90
 
    Returns disk utilization data for groups 3 and 7 over the last month where values exceed 90%.
 
.EXAMPLE
    1..5 | Get-WUGDeviceGroupReport -ReportType StateChange -Range today -SortBy deviceName -SortByDir asc
 
    Pipes group IDs 1 through 5 and retrieves today's state change report sorted by device name ascending.
 
.EXAMPLE
    Get-WUGDeviceGroupReport -GroupId 0 -ReportType Maintenance -ReturnHierarchy true
 
    Returns the maintenance mode report for all devices across the entire group hierarchy.
 
.NOTES
    Author: Jason Alberino (jason@wug.ninja)
    Reference: https://docs.ipswitch.com/NM/WhatsUpGold2024/02_Guides/rest_api/index.html#tag/DeviceGroupReport
#>

function Get-WUGDeviceGroupReport {
    [CmdletBinding()]
    param (
        [Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias('id')]
        [int[]]$GroupId = @(-2),

        [Parameter(Mandatory = $true)]
        [ValidateSet("Cpu", "Disk", "DiskSpaceFree", "Interface", "InterfaceDiscards", "InterfaceErrors", "InterfaceTraffic", "Memory", "PingAvailability", "PingResponseTime", "StateChange", "Maintenance")]
        [string]$ReportType,

        [ValidateSet("true", "false")]
        [string]$ReturnHierarchy,

        [ValidateSet("today", "lastPolled", "yesterday", "lastWeek", "lastMonth", "lastQuarter", "weekToDate", "monthToDate", "quarterToDate", "lastNSeconds", "lastNMinutes", "lastNHours", "lastNDays", "lastNWeeks", "lastNMonths", "custom")]
        [string]$Range,

        [string]$RangeStartUtc,
        [string]$RangeEndUtc,

        [int]$RangeN = 1,

        [string]$SortBy,

        [ValidateSet("asc", "desc")]
        [string]$SortByDir = "desc",

        [string]$GroupBy,

        [ValidateSet("asc", "desc")]
        [string]$GroupByDir,

        [ValidateSet("true", "false")]
        [string]$ApplyThreshold,

        [ValidateSet("true", "false")]
        [string]$OverThreshold,

        [double]$ThresholdValue,

        [int]$BusinessHoursId,

        [ValidateSet("true", "false")]
        [string]$RollupByDevice,

        [string]$PageId,

        [ValidateRange(0, 250)]
        [int]$Limit
    )

    begin {
        if (-not $global:WhatsUpServerBaseURI) {
            Write-Error "WhatsUpServerBaseURI is not set. Please run Connect-WUGServer to establish a connection."
            return
        }

        Write-Verbose "Starting Get-WUGDeviceGroupReport -ReportType $ReportType"

        $baseUri = "${global:WhatsUpServerBaseURI}/api/v1/device-groups"

        # Map ReportType to API endpoint and valid SortBy/GroupBy values
        $reportConfig = @{
            "Cpu" = @{
                Endpoint = "cpu-utilization"
                SortBy   = @("defaultColumn", "id", "deviceName", "cpu", "cpuId", "pollTimeUtc", "timeFromLastPollSeconds", "minPercent", "maxPercent", "avgPercent")
                GroupBy  = @("noGrouping", "id", "deviceName", "cpu", "cpuId", "pollTimeUtc", "timeFromLastPollSeconds", "minPercent", "maxPercent", "avgPercent")
            }
            "Disk" = @{
                Endpoint = "disk-utilization"
                SortBy   = @("defaultColumn", "id", "deviceName", "disk", "diskId", "pollTimeUtc", "timeFromLastPollSeconds", "size", "minUsed", "maxUsed", "avgUsed", "avgFree", "minPercent", "maxPercent", "avgPercent")
                GroupBy  = @("noGrouping", "id", "deviceName", "disk", "diskId", "pollTimeUtc", "timeFromLastPollSeconds", "size", "mi", "ma", "av")
            }
            "DiskSpaceFree" = @{
                Endpoint = "disk-free-space"
                SortBy   = @("defaultColumn", "id", "deviceName", "disk", "diskId", "pollTimeUtc", "timeFromLastPollSeconds", "size", "minFree", "maxFree", "avgFree")
                GroupBy  = @("noGrouping", "id", "deviceName", "disk", "diskId", "pollTimeUtc", "timeFromLastPollSeconds", "size", "minFree", "maxFree", "avgFree")
            }
            "Interface" = @{
                Endpoint = "interface-utilization"
                SortBy   = @("defaultColumn", "id", "deviceName", "interfaceName", "interfaceId", "pollTimeUtc", "timeFromLastPollSeconds", "rxMin", "rxMax", "rxAvg", "rxTotal", "txMin", "txMax", "txAvg", "txTotal", "totalAvg")
                GroupBy  = @("noGrouping", "id", "deviceName", "interfaceName", "interfaceId", "pollTimeUtc", "timeFromLastPollSeconds", "rxMin", "rxMax", "rxAvg", "rxTotal", "txMin", "txMax", "txAvg", "txTotal", "totalAvg")
            }
            "InterfaceDiscards" = @{
                Endpoint = "interface-discards"
                SortBy   = @("defaultColumn", "id", "deviceName", "interfaceName", "interfaceId", "pollTimeUtc", "timeFromLastPollSeconds", "rxMin", "rxMax", "rxAvg", "rxTotal", "txMin", "txMax", "txAvg", "txTotal", "totalAvg")
                GroupBy  = @("noGrouping", "id", "deviceName", "interfaceName", "interfaceId", "pollTimeUtc", "timeFromLastPollSeconds", "rxMin", "rxMax", "rxAvg", "rxTotal", "txMin", "txMax", "txAvg", "txTotal", "totalAvg")
            }
            "InterfaceErrors" = @{
                Endpoint = "interface-errors"
                SortBy   = @("defaultColumn", "id", "deviceName", "interfaceName", "interfaceId", "pollTimeUtc", "timeFromLastPollSeconds", "rxMin", "rxMax", "rxAvg", "rxTotal", "txMin", "txMax", "txAvg", "txTotal", "totalAvg")
                GroupBy  = @("noGrouping", "id", "deviceName", "interfaceName", "interfaceId", "pollTimeUtc", "timeFromLastPollSeconds", "rxMin", "rxMax", "rxAvg", "rxTotal", "txMin", "txMax", "txAvg", "txTotal", "totalAvg")
            }
            "InterfaceTraffic" = @{
                Endpoint = "interface-traffic"
                SortBy   = @("defaultColumn", "id", "deviceName", "interfaceName", "interfaceId", "pollTimeUtc", "timeFromLastPollSeconds", "rxMin", "rxMax", "rxAvg", "rxTotal", "txMin", "txMax", "txAvg", "txTotal", "totalAvg")
                GroupBy  = @("noGrouping", "id", "deviceName", "interfaceName", "interfaceId", "pollTimeUtc", "timeFromLastPollSeconds", "rxMin", "rxMax", "rxAvg", "rxTotal", "txMin", "txMax", "txAvg", "txTotal", "totalAvg")
            }
            "Memory" = @{
                Endpoint = "memory-utilization"
                SortBy   = @("defaultColumn", "id", "deviceName", "memory", "memoryId", "pollTimeUtc", "timeFromLastPollSeconds", "size", "minUsed", "maxUsed", "avgUsed", "minPercent", "maxPercent", "avgPercent")
                GroupBy  = @("noGrouping", "id", "deviceName", "memory", "memoryId", "pollTimeUtc", "timeFromLastPollSeconds", "size", "minUsed", "maxUsed", "avgUsed", "minPercent", "maxPercent", "avgPercent")
            }
            "PingAvailability" = @{
                Endpoint = "ping-availability"
                SortBy   = @("defaultColumn", "id", "deviceName", "interfaceId", "interfaceName", "packetsLost", "packetsSent", "percentAvailable", "percentPacketLoss", "totalTimeMinutes", "timeUnavailableMinutes", "pollTimeUtc", "timeFromLastPollSeconds")
                GroupBy  = @("noGrouping", "id", "deviceName", "interfaceId", "interfaceName", "packetsLost", "packetsSent", "percentAvailable", "percentPacketLoss", "totalTimeMinutes", "timeUnavailableMinutes", "pollTimeUtc", "timeFromLastPollSeconds")
            }
            "PingResponseTime" = @{
                Endpoint = "ping-response-time"
                SortBy   = @("defaultColumn", "id", "deviceName", "interfaceId", "interfaceName", "minMilliSec", "maxMilliSec", "avgMilliSec", "pollTimeUtc", "timeFromLastPollSeconds")
                GroupBy  = @("noGrouping", "id", "deviceName", "interfaceId", "interfaceName", "minMilliSec", "maxMilliSec", "avgMilliSec", "pollTimeUtc", "timeFromLastPollSeconds")
            }
            "StateChange" = @{
                Endpoint = "state-change"
                SortBy   = @("defaultColumn", "deviceName", "monitorTypeName", "stateName", "startTimeUtc", "endTimeUtc", "totalSeconds", "result")
                GroupBy  = @("noGrouping", "deviceName", "monitorTypeName", "stateName", "startTimeUtc", "endTimeUtc", "totalSeconds", "result")
            }
            "Maintenance" = @{
                Endpoint = "device-maintenance-mode"
                SortBy   = @("defaultColumn", "id", "name", "startTimeUtc", "durationSeconds", "maintenanceMode", "userName", "reason")
                GroupBy  = @("defaultColumn", "id", "name", "startTimeUtc", "durationSeconds", "maintenanceMode", "userName", "reason")
            }
        }

        $config = $reportConfig[$ReportType]
        $reportEndpoint = $config.Endpoint

        # Validate SortBy against the allowed values for this report type
        if ($SortBy -and $SortBy -notin $config.SortBy) {
            Write-Error "Invalid SortBy value '$SortBy' for report type '$ReportType'. Valid values: $($config.SortBy -join ', ')"
            return
        }

        # Validate GroupBy against the allowed values for this report type
        if ($GroupBy -and $GroupBy -notin $config.GroupBy) {
            Write-Error "Invalid GroupBy value '$GroupBy' for report type '$ReportType'. Valid values: $($config.GroupBy -join ', ')"
            return
        }

        # Build query string from bound parameters
        $queryParams = @{}
        if ($ReturnHierarchy)   { $queryParams["returnHierarchy"] = $ReturnHierarchy }
        if ($Range)             { $queryParams["range"] = $Range }
        if ($RangeStartUtc)     { $queryParams["rangeStartUtc"] = $RangeStartUtc }
        if ($RangeEndUtc)       { $queryParams["rangeEndUtc"] = $RangeEndUtc }
        if ($PSBoundParameters.ContainsKey('RangeN')) { $queryParams["rangeN"] = $RangeN }
        if ($SortBy)            { $queryParams["sortBy"] = $SortBy }
        if ($SortByDir)         { $queryParams["sortByDir"] = $SortByDir }
        if ($GroupBy)           { $queryParams["groupBy"] = $GroupBy }
        if ($GroupByDir)        { $queryParams["groupByDir"] = $GroupByDir }
        if ($PSBoundParameters.ContainsKey('ApplyThreshold')) { $queryParams["applyThreshold"] = $ApplyThreshold }
        if ($PSBoundParameters.ContainsKey('OverThreshold'))  { $queryParams["overThreshold"] = $OverThreshold }
        if ($PSBoundParameters.ContainsKey('ThresholdValue')) { $queryParams["thresholdValue"] = $ThresholdValue }
        if ($BusinessHoursId)   { $queryParams["businessHoursId"] = $BusinessHoursId }
        if ($PSBoundParameters.ContainsKey('RollupByDevice')) { $queryParams["rollupByDevice"] = $RollupByDevice }
        if ($PageId)            { $queryParams["pageId"] = $PageId }
        if ($PSBoundParameters.ContainsKey('Limit')) { $queryParams["limit"] = $Limit }

        $queryString = ($queryParams.GetEnumerator() | ForEach-Object { "$($_.Key)=$($_.Value)" }) -join "&"
        $collectedGroupIds = @()
        $finalOutput = @()
    }

    process {
        foreach ($id in $GroupId) {
            $collectedGroupIds += $id
        }
    }

    end {
        $totalGroups = $collectedGroupIds.Count
        $currentGroupIndex = 0

        foreach ($id in $collectedGroupIds) {
            $currentGroupIndex++
            $percentCompleteGroups = [Math]::Round(($currentGroupIndex / $totalGroups) * 100, 2)
            Write-Progress -Id 1 -Activity "Fetching $ReportType report for groups" -Status "Processing Group $currentGroupIndex of $totalGroups (GroupID: $id)" -PercentComplete $percentCompleteGroups

            $currentPageId = $null
            $pageCount = 0

            do {
                if ($currentPageId) {
                    $uri = "$baseUri/$id/devices/reports/$reportEndpoint/?pageId=$currentPageId"
                    if ($queryString) { $uri += "&$queryString" }
                } else {
                    $uri = "$baseUri/$id/devices/reports/$reportEndpoint/"
                    if ($queryString) { $uri += "?$queryString" }
                }

                Write-Verbose "API Call: $uri"

                try {
                    $result = Get-WUGAPIResponse -Uri $uri -Method "GET"
                    $finalOutput += $result.data
                    $currentPageId = $result.paging.nextPageId
                    $pageCount++

                    if ($result.paging.totalPages -and $result.paging.totalPages -gt 0) {
                        $percentCompletePage = [Math]::Round(($pageCount / $result.paging.totalPages) * 100, 2)
                        Write-Progress -Id 2 -Activity "Fetching $ReportType report for GroupID: $id" -Status "Page $pageCount of $($result.paging.totalPages)" -PercentComplete $percentCompletePage
                    } else {
                        Write-Progress -Id 2 -Activity "Fetching $ReportType report for GroupID: $id" -Status "Processing page $pageCount" -PercentComplete 0
                    }
                }
                catch {
                    Write-Error "Error fetching $ReportType report for GroupID ${id}: $_"
                    $currentPageId = $null
                }
            } while ($currentPageId)

            Write-Progress -Id 2 -Activity "Fetching $ReportType report for GroupID: $id" -Status "Completed" -Completed
        }

        Write-Progress -Id 1 -Activity "Fetching $ReportType report for groups" -Status "All groups processed" -Completed
        return $finalOutput
    }
}

# SIG # Begin signature block
# MIIVlwYJKoZIhvcNAQcCoIIViDCCFYQCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCBQd5h8Jzwaxc4X
# yjdkVPWSUzKcOzwVz96nA6CTKYDuEqCCEdMwggVvMIIEV6ADAgECAhBI/JO0YFWU
# jTanyYqJ1pQWMA0GCSqGSIb3DQEBDAUAMHsxCzAJBgNVBAYTAkdCMRswGQYDVQQI
# DBJHcmVhdGVyIE1hbmNoZXN0ZXIxEDAOBgNVBAcMB1NhbGZvcmQxGjAYBgNVBAoM
# EUNvbW9kbyBDQSBMaW1pdGVkMSEwHwYDVQQDDBhBQUEgQ2VydGlmaWNhdGUgU2Vy
# dmljZXMwHhcNMjEwNTI1MDAwMDAwWhcNMjgxMjMxMjM1OTU5WjBWMQswCQYDVQQG
# EwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMS0wKwYDVQQDEyRTZWN0aWdv
# IFB1YmxpYyBDb2RlIFNpZ25pbmcgUm9vdCBSNDYwggIiMA0GCSqGSIb3DQEBAQUA
# A4ICDwAwggIKAoICAQCN55QSIgQkdC7/FiMCkoq2rjaFrEfUI5ErPtx94jGgUW+s
# hJHjUoq14pbe0IdjJImK/+8Skzt9u7aKvb0Ffyeba2XTpQxpsbxJOZrxbW6q5KCD
# J9qaDStQ6Utbs7hkNqR+Sj2pcaths3OzPAsM79szV+W+NDfjlxtd/R8SPYIDdub7
# P2bSlDFp+m2zNKzBenjcklDyZMeqLQSrw2rq4C+np9xu1+j/2iGrQL+57g2extme
# me/G3h+pDHazJyCh1rr9gOcB0u/rgimVcI3/uxXP/tEPNqIuTzKQdEZrRzUTdwUz
# T2MuuC3hv2WnBGsY2HH6zAjybYmZELGt2z4s5KoYsMYHAXVn3m3pY2MeNn9pib6q
# RT5uWl+PoVvLnTCGMOgDs0DGDQ84zWeoU4j6uDBl+m/H5x2xg3RpPqzEaDux5mcz
# mrYI4IAFSEDu9oJkRqj1c7AGlfJsZZ+/VVscnFcax3hGfHCqlBuCF6yH6bbJDoEc
# QNYWFyn8XJwYK+pF9e+91WdPKF4F7pBMeufG9ND8+s0+MkYTIDaKBOq3qgdGnA2T
# OglmmVhcKaO5DKYwODzQRjY1fJy67sPV+Qp2+n4FG0DKkjXp1XrRtX8ArqmQqsV/
# AZwQsRb8zG4Y3G9i/qZQp7h7uJ0VP/4gDHXIIloTlRmQAOka1cKG8eOO7F/05QID
# AQABo4IBEjCCAQ4wHwYDVR0jBBgwFoAUoBEKIz6W8Qfs4q8p74Klf9AwpLQwHQYD
# VR0OBBYEFDLrkpr/NZZILyhAQnAgNpFcF4XmMA4GA1UdDwEB/wQEAwIBhjAPBgNV
# HRMBAf8EBTADAQH/MBMGA1UdJQQMMAoGCCsGAQUFBwMDMBsGA1UdIAQUMBIwBgYE
# VR0gADAIBgZngQwBBAEwQwYDVR0fBDwwOjA4oDagNIYyaHR0cDovL2NybC5jb21v
# ZG9jYS5jb20vQUFBQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmwwNAYIKwYBBQUHAQEE
# KDAmMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5jb21vZG9jYS5jb20wDQYJKoZI
# hvcNAQEMBQADggEBABK/oe+LdJqYRLhpRrWrJAoMpIpnuDqBv0WKfVIHqI0fTiGF
# OaNrXi0ghr8QuK55O1PNtPvYRL4G2VxjZ9RAFodEhnIq1jIV9RKDwvnhXRFAZ/ZC
# J3LFI+ICOBpMIOLbAffNRk8monxmwFE2tokCVMf8WPtsAO7+mKYulaEMUykfb9gZ
# pk+e96wJ6l2CxouvgKe9gUhShDHaMuwV5KZMPWw5c9QLhTkg4IUaaOGnSDip0TYl
# d8GNGRbFiExmfS9jzpjoad+sPKhdnckcW67Y8y90z7h+9teDnRGWYpquRRPaf9xH
# +9/DUp/mBlXpnYzyOmJRvOwkDynUWICE5EV7WtgwggYaMIIEAqADAgECAhBiHW0M
# UgGeO5B5FSCJIRwKMA0GCSqGSIb3DQEBDAUAMFYxCzAJBgNVBAYTAkdCMRgwFgYD
# VQQKEw9TZWN0aWdvIExpbWl0ZWQxLTArBgNVBAMTJFNlY3RpZ28gUHVibGljIENv
# ZGUgU2lnbmluZyBSb290IFI0NjAeFw0yMTAzMjIwMDAwMDBaFw0zNjAzMjEyMzU5
# NTlaMFQxCzAJBgNVBAYTAkdCMRgwFgYDVQQKEw9TZWN0aWdvIExpbWl0ZWQxKzAp
# BgNVBAMTIlNlY3RpZ28gUHVibGljIENvZGUgU2lnbmluZyBDQSBSMzYwggGiMA0G
# CSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQCbK51T+jU/jmAGQ2rAz/V/9shTUxjI
# ztNsfvxYB5UXeWUzCxEeAEZGbEN4QMgCsJLZUKhWThj/yPqy0iSZhXkZ6Pg2A2NV
# DgFigOMYzB2OKhdqfWGVoYW3haT29PSTahYkwmMv0b/83nbeECbiMXhSOtbam+/3
# 6F09fy1tsB8je/RV0mIk8XL/tfCK6cPuYHE215wzrK0h1SWHTxPbPuYkRdkP05Zw
# mRmTnAO5/arnY83jeNzhP06ShdnRqtZlV59+8yv+KIhE5ILMqgOZYAENHNX9SJDm
# +qxp4VqpB3MV/h53yl41aHU5pledi9lCBbH9JeIkNFICiVHNkRmq4TpxtwfvjsUe
# dyz8rNyfQJy/aOs5b4s+ac7IH60B+Ja7TVM+EKv1WuTGwcLmoU3FpOFMbmPj8pz4
# 4MPZ1f9+YEQIQty/NQd/2yGgW+ufflcZ/ZE9o1M7a5Jnqf2i2/uMSWymR8r2oQBM
# dlyh2n5HirY4jKnFH/9gRvd+QOfdRrJZb1sCAwEAAaOCAWQwggFgMB8GA1UdIwQY
# MBaAFDLrkpr/NZZILyhAQnAgNpFcF4XmMB0GA1UdDgQWBBQPKssghyi47G9IritU
# pimqF6TNDDAOBgNVHQ8BAf8EBAMCAYYwEgYDVR0TAQH/BAgwBgEB/wIBADATBgNV
# HSUEDDAKBggrBgEFBQcDAzAbBgNVHSAEFDASMAYGBFUdIAAwCAYGZ4EMAQQBMEsG
# A1UdHwREMEIwQKA+oDyGOmh0dHA6Ly9jcmwuc2VjdGlnby5jb20vU2VjdGlnb1B1
# YmxpY0NvZGVTaWduaW5nUm9vdFI0Ni5jcmwwewYIKwYBBQUHAQEEbzBtMEYGCCsG
# AQUFBzAChjpodHRwOi8vY3J0LnNlY3RpZ28uY29tL1NlY3RpZ29QdWJsaWNDb2Rl
# U2lnbmluZ1Jvb3RSNDYucDdjMCMGCCsGAQUFBzABhhdodHRwOi8vb2NzcC5zZWN0
# aWdvLmNvbTANBgkqhkiG9w0BAQwFAAOCAgEABv+C4XdjNm57oRUgmxP/BP6YdURh
# w1aVcdGRP4Wh60BAscjW4HL9hcpkOTz5jUug2oeunbYAowbFC2AKK+cMcXIBD0Zd
# OaWTsyNyBBsMLHqafvIhrCymlaS98+QpoBCyKppP0OcxYEdU0hpsaqBBIZOtBajj
# cw5+w/KeFvPYfLF/ldYpmlG+vd0xqlqd099iChnyIMvY5HexjO2AmtsbpVn0OhNc
# WbWDRF/3sBp6fWXhz7DcML4iTAWS+MVXeNLj1lJziVKEoroGs9Mlizg0bUMbOalO
# hOfCipnx8CaLZeVme5yELg09Jlo8BMe80jO37PU8ejfkP9/uPak7VLwELKxAMcJs
# zkyeiaerlphwoKx1uHRzNyE6bxuSKcutisqmKL5OTunAvtONEoteSiabkPVSZ2z7
# 6mKnzAfZxCl/3dq3dUNw4rg3sTCggkHSRqTqlLMS7gjrhTqBmzu1L90Y1KWN/Y5J
# KdGvspbOrTfOXyXvmPL6E52z1NZJ6ctuMFBQZH3pwWvqURR8AgQdULUvrxjUYbHH
# j95Ejza63zdrEcxWLDX6xWls/GDnVNueKjWUH3fTv1Y8Wdho698YADR7TNx8X8z2
# Bev6SivBBOHY+uqiirZtg0y9ShQoPzmCcn63Syatatvx157YK9hlcPmVoa1oDE5/
# L9Uo2bC5a4CH2RwwggY+MIIEpqADAgECAhAHnODk0RR/hc05c892LTfrMA0GCSqG
# SIb3DQEBDAUAMFQxCzAJBgNVBAYTAkdCMRgwFgYDVQQKEw9TZWN0aWdvIExpbWl0
# ZWQxKzApBgNVBAMTIlNlY3RpZ28gUHVibGljIENvZGUgU2lnbmluZyBDQSBSMzYw
# HhcNMjYwMjA5MDAwMDAwWhcNMjkwNDIxMjM1OTU5WjBVMQswCQYDVQQGEwJVUzEU
# MBIGA1UECAwLQ29ubmVjdGljdXQxFzAVBgNVBAoMDkphc29uIEFsYmVyaW5vMRcw
# FQYDVQQDDA5KYXNvbiBBbGJlcmlubzCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCC
# AgoCggIBAPN6aN4B1yYWkI5b5TBj3I0VV/peETrHb6EY4BHGxt8Ap+eT+WpEpJyE
# tRYPxEmNJL3A38Bkg7mwzPE3/1NK570ZBCuBjSAn4mSDIgIuXZnvyBO9W1OQs5d6
# 7MlJLUAEufl18tOr3ST1DeO9gSjQSAE5Nql0QDxPnm93OZBon+Fz3CmE+z3MwAe2
# h4KdtRAnCqwM+/V7iBdbw+JOxolpx+7RVjGyProTENIG3pe/hKvPb501lf8uBAAD
# LdjZr5ip8vIWbf857Yw1Bu10nVI7HW3eE8Cl5//d1ribHlzTzQLfttW+k+DaFsKZ
# BBL56l4YAlIVRsrOiE1kdHYYx6IGrEA809R7+TZA9DzGqyFiv9qmJAbL4fDwetDe
# yIq+Oztz1LvEdy8Rcd0JBY+J4S0eDEFIA3X0N8VcLeAwabKb9AjulKXwUeqCJLvN
# 79CJ90UTZb2+I+tamj0dn+IKMEsJ4v4Ggx72sxFr9+6XziodtTg5Luf2xd6+Phha
# mOxF2px9LObhBLLEMyRsCHZIzVZOFKu9BpHQH7ufGB+Sa80Tli0/6LEyn9+bMYWi
# 2ttn6lLOPThXMiQaooRUq6q2u3+F4SaPlxVFLI7OJVMhar6nW6joBvELTJPmANSM
# jDSRFDfHRCdGbZsL/keELJNy+jZctF6VvxQEjFM8/bazu6qYhrA7AgMBAAGjggGJ
# MIIBhTAfBgNVHSMEGDAWgBQPKssghyi47G9IritUpimqF6TNDDAdBgNVHQ4EFgQU
# 6YF0o0D5AVhKHbVocr8GaSIBibAwDgYDVR0PAQH/BAQDAgeAMAwGA1UdEwEB/wQC
# MAAwEwYDVR0lBAwwCgYIKwYBBQUHAwMwSgYDVR0gBEMwQTA1BgwrBgEEAbIxAQIB
# AwIwJTAjBggrBgEFBQcCARYXaHR0cHM6Ly9zZWN0aWdvLmNvbS9DUFMwCAYGZ4EM
# AQQBMEkGA1UdHwRCMEAwPqA8oDqGOGh0dHA6Ly9jcmwuc2VjdGlnby5jb20vU2Vj
# dGlnb1B1YmxpY0NvZGVTaWduaW5nQ0FSMzYuY3JsMHkGCCsGAQUFBwEBBG0wazBE
# BggrBgEFBQcwAoY4aHR0cDovL2NydC5zZWN0aWdvLmNvbS9TZWN0aWdvUHVibGlj
# Q29kZVNpZ25pbmdDQVIzNi5jcnQwIwYIKwYBBQUHMAGGF2h0dHA6Ly9vY3NwLnNl
# Y3RpZ28uY29tMA0GCSqGSIb3DQEBDAUAA4IBgQAEIsm4xnOd/tZMVrKwi3doAXvC
# wOA/RYQnFJD7R/bSQRu3wXEK4o9SIefye18B/q4fhBkhNAJuEvTQAGfqbbpxow03
# J5PrDTp1WPCWbXKX8Oz9vGWJFyJxRGftkdzZ57JE00synEMS8XCwLO9P32MyR9Z9
# URrpiLPJ9rQjfHMb1BUdvaNayomm7aWLAnD+X7jm6o8sNT5An1cwEAob7obWDM6s
# X93wphwJNBJAstH9Ozs6LwISOX6sKS7CKm9N3Kp8hOUue0ZHAtZdFl6o5u12wy+z
# zieGEI50fKnN77FfNKFOWKlS6OJwlArcbFegB5K89LcE5iNSmaM3VMB2ADV1FEcj
# GSHw4lTg1Wx+WMAMdl/7nbvfFxJ9uu5tNiT54B0s+lZO/HztwXYQUczdsFon3pjs
# Nrsk9ZlalBi5SHkIu+F6g7tWiEv3rtVApmJRnLkUr2Xq2a4nbslUCt4jKs5UX4V1
# nSX8OM++AXoyVGO+iTj7z+pl6XE9Gw/Td6WKKKsxggMaMIIDFgIBATBoMFQxCzAJ
# BgNVBAYTAkdCMRgwFgYDVQQKEw9TZWN0aWdvIExpbWl0ZWQxKzApBgNVBAMTIlNl
# Y3RpZ28gUHVibGljIENvZGUgU2lnbmluZyBDQSBSMzYCEAec4OTRFH+FzTlzz3Yt
# N+swDQYJYIZIAWUDBAIBBQCggYQwGAYKKwYBBAGCNwIBDDEKMAigAoAAoQKAADAZ
# BgkqhkiG9w0BCQMxDAYKKwYBBAGCNwIBBDAcBgorBgEEAYI3AgELMQ4wDAYKKwYB
# BAGCNwIBFTAvBgkqhkiG9w0BCQQxIgQgiObQFIsUF2aK14MDB0ePqWhzUgOKkKBo
# QtPUW9+EybswDQYJKoZIhvcNAQEBBQAEggIAbC2E1sh4P+ZozZeL7hfD1HQlMmRS
# tE/Y9Axf/iLhcr3dUyDhzfTM0IIvwLIwm2g/KxWo/s+wlULuyO+EKiAkdCLgw+dw
# JrY11LOBRdN5oH/A5G8LIAbjMe2vXDtzM2G6sTmAuXCmi4DXNyK/rHPAuoLyBhk2
# I2kcGqGB2cSD9b7s6rVOA75wvROlE+2og9Tkqe/ZTNyhY5z4sjVXRVpBxyrhIvlF
# HrFLvsERotLXSPtW2jYkCtZwXgUIeYxbM+qJzNxet2NOJGP+K3QoenKRMXf+hRqW
# +RcT/TSVujxoS6Lv8yzVKk0MHu8HKRy0Ior8oL5AaAET2i6Kl8RJC1RrArVanRyJ
# b2vQN4DXpbopj7ziY1hthGhusHdXYAEJU2H+FtUIBpGBcfW5isgcA0UPHjF7Yvp9
# buA2cZBHluJBOo0smor947nnmOxGRULJ/dg2sIovh/2Gh7rt4VIkTvhUh3Bf7Gud
# kWZPMKJistEE8L6f2rCaHpMozInD0uHRPf+OVxPbFtz8Abs3teB+8jSX00ptvlB1
# mPTjH3TYteh5AFKRtal/pWOskFqe0wy/25q+wMseKbmoJXyUJCNf3QYLCH2wY0V0
# cOlZq3mZNNxK82/RcoSHplq6YpjZK8p/1L8F5k5C3/w9GLEUUCPagEF/275Tl/w/
# l8q2kuuP/LlTIro=
# SIG # End signature block