CoveBackupApi.psm1

#Region './Private/Convert-CoveUnixTime.ps1' -1

function Convert-CoveUnixTime {
    <#
    .SYNOPSIS
        A helper functions used to convert unix timestamps to or from DateTime objects
    .DESCRIPTION
        The Cove API provides timestamps in unix format, this function converts them to DateTime objects
    .EXAMPLE
        Convert-CoveUnixTime -UnixTime 1685522071
        Converts the unix timestamp 1685522071 to a DateTime object
    .OUTPUTS
        System.DateTime or
        System.Int32 unix timestamp
    #>



    [CmdletBinding()]
    param (
        # one of either the UnixTime or DateTime parameters must be specified
        [Parameter(Mandatory, ParameterSetName = 'UnixTime')]
        # The unix timestamp to convert
        [Parameter()]
        [int]$UnixTime,
        [Parameter(Mandatory, ParameterSetName = 'DateTime')]
        # The DateTime object to convert
        [Parameter()]
        [datetime]$DateTime

    )

    begin {

    }

    process {
        $Epoch = New-Object DateTime 1970, 1, 1, 0, 0, 0, ([DateTimeKind]::Utc)
        if ($UnixTime) {
            return $Epoch.AddSeconds($UnixTime)
        }
        elseif ($DateTime) {
            return ($DateTime - $Epoch).TotalSeconds
        }
    }

    end {

    }
}
#EndRegion './Private/Convert-CoveUnixTime.ps1' 48
#Region './Private/Get-CoveBackupStatusMap.ps1' -1

function Get-CoveBackupStatusMap {
    <#
    .SYNOPSIS
        Gets the status descriptions for the numeric values returned from the Cove API
    .DESCRIPTION
        Returns objects with the status descriptions for the numeric values returned from the Cove API
    .PARAMETER FieldName
        The field name to return the key for
    .EXAMPLE
        Get-CoveBackupStatusMap
        Gets the full map
    .EXAMPLE
        Get-CoveBackupStatusMap -FieldName 'In Progress'
        Returns the numeric value for the status label 'In Progress'
        > 1
    .OUTPUTS
        System.Collections.Hashtable
    #>



    [CmdletBinding()]
    param (
        # Get the column header for a specific field name
        [Parameter()]
        [string]
        $FieldName
    )

    begin {

    }

    process {

        $DataMap = @{
            1     = 'In Progress'
            2     = 'Failed'
            3     = 'Aborted'
            5     = 'Completed'
            6     = 'Interrupted'
            7     = 'Not Started'
            8     = 'Completed With Errors'
            9     = 'In Progress With Faults'
            10    = 'Over Quota'
            11    = 'No Selection'
            12    = 'Restarted'
        }
        if ($FieldName) {
            try {
                $NumericValue = $DataMap.GetEnumerator() | Where-Object { $_.Value -like $FieldName } | Select-Object -ExpandProperty Key
                return $NumericValue
            }
            catch {
                Write-Warning "Failed to find a column header for $FieldName"
                return $null
            }
        }
        return $DataMap
    }

    end {

    }
}
#EndRegion './Private/Get-CoveBackupStatusMap.ps1' 65
#Region './Private/Get-CoveDataMap.ps1' -1

function Get-CoveDataMap {
    <#
    .SYNOPSIS
        Gets the data maps for the Cove API
    .DESCRIPTION
        Returns objects with the data map for the Cove API given the parameter passed
    .PARAMETER DataMap
        The data map to return
    .PARAMETER FieldName
        The field name to return the column ID for
    .EXAMPLE
        Get-CoveDataMap
        Gets the full data map
    .EXAMPLE
        Get-CoveDataMap -DataMap DataSources
        Gets the backup data source types
    .EXAMPLE
        Get-CoveDataMap -DataMap ColumnHeaders
        Gets the column headers returned from the API
    .EXAMPLE
        Get-CoveDataMap -FieldName 'Hyper-V'
        Returns the column ID for the Hyper-V field (D14)
    .OUTPUTS
        System.Collections.Hashtable
    #>



    [CmdletBinding()]
    param (
        # The data map to return
        [Parameter()]
        [ValidateSet('DataSources','ColumnHeaders','StatsFields')]
        [string]$DataMap,
        # Get the column header for a specific field name
        [Parameter()]
        [string]
        $FieldName

    )

    begin {

    }

    process {

        $FullMap = @{
            D01    = 'Files and Folders'
            D02    = 'System State'
            D03    = 'MsSql'
            D04    = 'VssExchange'
            D05    = 'Microsoft 365 SharePoint'
            D06    = 'NetworkShares'
            D07    = 'VssSystemState'
            D08    = 'VMware Virtual Machines'
            D09    = 'Total'
            D10   = 'VssMsSql'
            D11   = 'VssSharePoint'
            D12   = 'Oracle'
            D14   = 'Hyper-V'
            D15   = 'MySql'
            D16   = 'Virtual Disaster Recovery'
            D17   = 'Bare Metal Restore'
            D19   = 'Microsoft 365 Exchange'
            D20   = 'Microsoft 365 OneDrive'
            D23   = 'Microsoft 365 Teams'
            F00   = 'Last Session Status' # 1 - In process, 2 - Failed, 3 - Aborted, 5 - Completed, 6 - Interrupted, 7 - NotStarted, 8 - CompletedWithErrors, 9 - InProgressWithFaults, 10 - OverQuota, 11 - NoSelection, 12 - Restarted
            F01   = 'Last Session Selected Count'
            F02   = 'Last Session Processed Count'
            F03   = 'Last Session Selected Size'
            F04   = 'Last Session Processed Size'
            F05   = 'Last Session Sent Size'
            F06   = 'Last Session Errors Count'
            F07   = 'Protected size'
            F08   = 'Color bar - last 28 days'
            F09   = 'Last successful session Timestamp'
            F10   = 'Pre Recent Session Selected Count'
            F11   = 'Pre Recent Session Selected Size'
            F12   = 'Session duration'
            F13   = 'Last Session License Items count'
            F14   = 'Retention'
            F15   = 'Last Session Timestamp'
            F16   = 'Last Successful Session Status'
            F17   = 'Last Completed Session Status'
            F18   = 'Last Completed Session Timestamp'
            F19   = 'Last Session Verification Data'
            F20   = 'Last Session User Mailboxes Count'
            F21   = 'Last Session Shared Mailboxes Count'
            I0   = 'Device ID'
            I1   = 'Device name'
            I2   = 'Device name alias'
            I3   = 'Password'
            I4   = 'Creation date'
            I5   = 'Expiration date'
            I6   = 'Timestamp' # time
            I8   = 'Customer'
            I9   = 'Product ID'
            I10   = 'Product'
            I11   = 'Storage location'
            I12   = 'Device group name'
            I13   = 'Own user name'
            I14   = 'Used storage'
            I15   = 'Email'
            I16   = 'OS version'
            I17   = 'Client version'
            I18   = 'Computer name'
            I19   = 'Internal IPs'
            I20   = 'External IPs'
            I21   = 'MAC address'
            I22   = 'Dashboard frequency'
            I23   = 'Dashboard language'
            I24   = 'Time offset'
            I26   = 'Cabinet Storage Efficiency'
            I27   = 'Total Cabinets Count'
            I28   = 'Efficient Cabinet Count 0-25'
            I29   = 'Efficient Cabinet Count 26-50'
            I30   = 'Efficient Cabinet Count 50-75'
            I31   = 'Used Virtual Storage'
            I32   = 'OS type' # 1 - workstation, 2 - server, 0 - undefined
            I33   = 'Seeding mode' # 0 - Undefined, 1 - Normal, 2 - Seeding, 3 - PreSeeding, 4 - PostSeeding
            I34   = 'Anti Crypto enabled'
            I35   = 'LSV' # 0 - Disabled, 1 - Enabled
            I36   = 'Storage status' # 2 - Offline, 1 - Failed, 0 - Undefined, 50 - Running, 100 - Synchronized
            I37   = 'LSV status' # 2 - Offline, 1 - Failed, 0 - Undefined, 50 - Running, 100 - Synchronized
            I38   = 'Archived size'
            I39   = 'Retention units'
            I40   = 'Activity description'
            I41   = 'Number of Hyper-V virtual machines'
            I42   = 'Number of ESX virtual machines'
            I43   = 'Encryption status'
            I44   = 'Computer manufacturer'
            I45   = 'Computer model'
            I46   = 'Installation ID'
            I47   = 'Installation Mode'
            I48   = 'Restore email'
            I49   = 'Restore dashboard frequency'
            I50   = 'Restore dashboards language'
            I54   = 'Profile ID'
            I55   = 'Profile version'
            I56   = 'Profile'
            I57   = 'Stock Keeping Unit'
            I58   = 'Stock Keeping Unit of the previous month'
            I59   = 'Account type'
            I60   = 'Proxy Type'
            I62   = 'Most Recent Restore Plugin'
            I63   = 'Company Name'
            I64   = 'Address'
            I65   = 'Zip Code'
            I66   = 'Country'
            I67   = 'City'
            I68   = 'Phone Number'
            I69   = 'Fax Number'
            I70   = 'Contract Name'
            I71   = 'Group Name'
            I72   = 'Demo'
            I73   = 'Edu'
            I74   = 'Unattended Installation account ID'
            I75   = 'First Installation Flag'
            I76   = 'Maximum Allowed Version'
            I77   = 'Customer reference'
            I78   = 'Active data sources'
            I80   = 'Recovery Testing'
            I81   = 'Physicality'
            I82   = 'Passphrase'

        }
        if ($FieldName) {
            try {
                $ColumnHeaders = $FullMap.GetEnumerator() | Where-Object { $_.Value -like $FieldName } | Select-Object -ExpandProperty Key
                return $ColumnHeaders
            }
            catch {
                Write-Error "Failed to find a column header for $FieldName"
                return $null
            }
        }
        if ($DataMap -eq 'DataSources') {
            $Map = $FullMap.GetEnumerator() | Where-Object { $_.Name -like 'D*' }
        }
        if ($DataMap -eq 'StatsFields') {
            $Map = $FullMap.GetEnumerator() | Where-Object { $_.Name -like 'F*' }
        }
        if ($DataMap -eq 'ColumnHeaders') {
            $Map = $FullMap.GetEnumerator() | Where-Object { $_.Name -like 'I*' }
        }
        if (!$DataMap) {
            $Map = $FullMap
        }

        return $Map | Sort-Object -Property Value
    }

    end {

    }
}
#EndRegion './Private/Get-CoveDataMap.ps1' 197
#Region './Private/Get-CoveUnixTimeFields.ps1' -1

function Get-CoveUnixTimeFields {
    <#
    .SYNOPSIS
        Gets the Unix fields for the Cove API
    .DESCRIPTION
        Gets the Unix fields for the Cove API
    .EXAMPLE
        Get-CoveUnixFields -Verbose
        Returns the Unix fields for the Cove API
    .OUTPUTS
        System.Object[]
    #>

    [CmdletBinding()]
    [OutputType([System.Object[]])]
    param (

    )

    begin {

    }

    process {
        $UnixTimeFields = @(
            'CreationTime',
            'ExpirationTime',
            'TrialExpirationTime',
            'TrialRegistrationTime',
            'Timestamp',
            'I6',
            'Expiration date',
            'I5',
            'Creation date',
            'I4',
            'F09', # Last successful session timestamp
            'F15', # Last session timestamp
            'F18' # Last completed session timestamp
        )
        return $UnixTimeFields

    }

    end {

    }
}
#EndRegion './Private/Get-CoveUnixTimeFields.ps1' 47
#Region './Private/Invoke-CoveApiRequest.ps1' -1

function Invoke-CoveApiRequest {
    <#
    .SYNOPSIS
        Performs a request to the Cove API
    .DESCRIPTION
        A private function, will invoke a request to the Cove API when used within the CoveBackupApi module
    .EXAMPLE
        $params = @{
            CoveMethod = 'EnumeratePartners'
            Params = @{
                parentPartnerId = 123456
                fields = @(0,1,3,4,5,8,9,10,18,20)
                fetchRecursively = $true
            }
            Id = 'jsonrpc'
        }
        $CoveCompanies = Invoke-CoveApiRequest @params
        Gets all companies from the Cove API where the Partner ID is 123456
    .OUTPUTS
        System.Management.Automation.PSCustomObject
    #>



    [CmdletBinding()]
    param (
        # The request method to call
        [Parameter()]
        [string]$Method = 'POST',
        # The Cove method to call
        [Parameter()]
        [string]$CoveMethod,
        # The parameters to pass to the method
        [Parameter()]
        [hashtable]$Params,
        # The url to override the default
        [Parameter()]
        [string]$UrlOverride,
        # The id of the request to pass
        [Parameter()]
        [string]$Id = 2

    )

    begin {
        if (!(Test-CoveApiVisa)) {
            New-CoveApiSession
        }

    }

    process {
        $RequestParams = @{
            ContentType = 'application/json'
            Method = $Method
            Body = @{
                jsonrpc = '2.0'
                method = $CoveMethod
                id = $Id
                visa = $Script:CoveApiSession.visa
                params = $Params
            } | ConvertTo-Json -Depth 20
            Uri = $UrlOverride ? $UrlOverride : $Script:CoveApiCredentials.Url
            UseBasicParsing = $true
            SessionVariable = 'CoveSession'
        }
        Write-Debug "Sending request to $CoveMethod via $Method $($RequestParams | ConvertTo-Json -Depth 10)"
        try {
            $Request = Invoke-WebRequest @RequestParams
        }
        catch {
            Write-Debug "Request to $CoveMethod via $Method $($Request | ConvertTo-Json -Depth 10)"
            Throw "Failed to access $CoveMethod via $Method with: $($_.Exception.Message)"
        }


        if ($Request.StatusCode -ne 200) {
            Write-Debug "Request to $CoveMethod via $Method $($Request | ConvertTo-Json -Depth 10)"
            Throw "Failed to access $CoveMethod via $Method with: $($Request.StatusCode) - $($Request.StatusDescription)"
        }

        try {
            $Response = $Request | ConvertFrom-Json
        }
        catch {
            Write-Debug "Request to $CoveMethod via $Method $($Request | ConvertTo-Json -Depth 10)"
            Throw "Failed to parse the response from $CoveMethod via $Method with: $($_.Exception.Message)"
        }



        if ($Response.error) {
            Write-Debug "Request to $CoveMethod via $Method $($Request | ConvertTo-Json -Depth 10)"
            Write-Debug "Response from $CoveMethod via $Method $($Response | ConvertTo-Json -Depth 10)"
            Throw "Failed to access $CoveMethod via $Method with: $($Response.error.message)"
        }

        $Data = $Response.result.result
        if ($Data) {
            #extend the visa stored in the session
            $Script:CoveApiSession.visa = $Response.visa
            return $Data
        }

        Write-Verbose "No data returned from $CoveMethod via $Method"

    }

    end {

    }
}
#EndRegion './Private/Invoke-CoveApiRequest.ps1' 112
#Region './Private/Test-CoveApiVisa.ps1' -1

function Test-CoveApiVisa {
    <#
    .SYNOPSIS
        Tests to see if a valid visa exists for the Cove API
    .DESCRIPTION
        Checks for an existing valid visa
    .EXAMPLE
        Test-CoveApiVisa -Verbose
        Uses the script's default credentials to test for a valid visa
    .OUTPUTS
        System.Boolean
    #>

    [OutputType([System.Boolean])]
    [CmdletBinding()]
    param (

    )

    begin {

    }

    process {
        if (!$Script:CoveApiSession) {
            Write-Verbose "No visa found"
            return $false
        }
        if ($Script:CoveApiSession.validfrom -lt (Get-Date).ToUniversalTime().AddMinutes(-10)) {
            Write-Verbose "Visa expired, valid until $(($Script:CoveApiSession.validfrom).AddMinutes(15))"
            return $false
        }
        Write-Verbose "Visa found, valid until $(($Script:CoveApiSession.validfrom).AddMinutes(15))"
        return $true
    }

    end {

    }
}
#EndRegion './Private/Test-CoveApiVisa.ps1' 40
#Region './Public/Get-CoveCompany.ps1' -1

function Get-CoveCompany {
    <#
    .SYNOPSIS
        Gets companies from the Cove API
    .DESCRIPTION
        Gets companies from the Cove API, using the credentials stored in the script
    .EXAMPLE
        Get-CoveCompany -Verbose
        Gets all companies from the Cove API
    .EXAMPLE
        Get-CoveCompany -ParentPartnerId 123456 -Verbose
        Gets companies from the Cove API, where the parent partner ID is 123456
    .EXAMPLE
        Get-CoveCompany -CompanyId 123456 -Verbose
        Gets the company with ID 123456 from the Cove API
    .OUTPUTS
        System.Management.Automation.PSCustomObject
    #>

    [CmdletBinding()]
    param (
        # The ID of the partner to get companies for
        [Parameter()]
        [int]$ParentPartnerId,
        # The ID of the company to get
        [Parameter()]
        [int]$CompanyId,
        # Whether to return only sites, default is to return companies
        [Parameter()]
        [switch]$Sites
    )

    begin {

    }

    process {
        $params = @{
            CoveMethod = 'EnumeratePartners'
            Params = @{
                parentPartnerId = $ParentPartnerId ? $ParentPartnerId : $Script:CoveApiSession.PartnerInfo.id
                fields = @(0,1,3,4,5,8,9,10,18,20)
                fetchRecursively = $true
            }
            Id = 'jsonrpc'
        }

        $Data = Invoke-CoveApiRequest @params
        if ($Data) {
            $UnixTimeFields = Get-CoveUnixTimeFields
            foreach ($Company in $Data.GetEnumerator()) {
                foreach ($Property in $Company.psobject.Properties) {
                    if ($Property.Name -in $UnixTimeFields) {
                        $Property.Value = Convert-CoveUnixTime -UnixTime $Property.Value
                    }
                }
            }

            if ($CompanyId) {
                # This endpoint does not support filtering by ID, so we have to do it ourselves
                return $Data | Where-Object {$_.Id -eq $CompanyId}
            }
            if ($Sites) {
                return $Data | Where-Object {$_.Level -eq 'Site'}
            }
            else {
                return $Data | Where-Object {$_.Level -ne 'Site'}
            }
        }

    }

    end {

    }
}
#EndRegion './Public/Get-CoveCompany.ps1' 76
#Region './Public/Get-CoveCompanyInfo.ps1' -1

function Get-CoveCompanyInfo {
    <#
    .SYNOPSIS
        Gets detailed company info from the Cove API
    .DESCRIPTION
        Gets information fro the Cove API for a specified company, using the credentials stored in the script
    .PARAMETER CompanyId
        The ID of the company to get info for
    .EXAMPLE
        Get-CoveCompanyInfo -CompanyId 12345 -Verbose
        Gets information for the company with ID 12345 from the Cove API
    .OUTPUTS
        System.Management.Automation.PSCustomObject
    #>

    [CmdletBinding()]
    param (
        # The ID of the company to get information for
        [Parameter(Mandatory)]
        [int]$CompanyId
    )

    begin {

    }

    process {
        $params = @{
            CoveMethod = 'GetPartnerInfoById'
            Params = @{
                partnerId = $CompanyId
            }
            Id = 'jsonrpc'
        }



        $Data = Invoke-CoveApiRequest @params
        if ($Data) {
            $UnixTimeFields = Get-CoveUnixTimeFields
            foreach ($Property in $Data.psobject.Properties) {
                if ($Property.Name -in $UnixTimeFields) {
                    $Property.Value = Convert-CoveUnixTime -UnixTime $Property.Value
                }
            }
            return $Data
        }

    }

    end {

    }
}
#EndRegion './Public/Get-CoveCompanyInfo.ps1' 54
#Region './Public/Get-CoveDevice.ps1' -1

function Get-CoveDevice {
    <#
    .SYNOPSIS
        Gets devices from the Cove API
    .DESCRIPTION
        Gets devices from the Cove API, using the credentials stored in the script
    .EXAMPLE
        Get-CoveDevice -Verbose
        Gets all devices from the Cove API
    .EXAMPLE
        Get-CoveDevice -PartnerId 1234 -Verbose
        Gets devices from the Cove API for the partner with ID 1234
    .EXAMPLE
        Get-CoveDevice -DeviceId 1234 -Verbose
        Gets the device with ID 1234 from the Cove API
    .OUTPUTS
        System.Management.Automation.PSCustomObject
    #>

    [CmdletBinding()]
    param (
        # The ID of the device to get
        [Parameter()]
        [int]$DeviceId,
        # The ID of the partner to get devices for
        [Parameter()]
        [int]$PartnerId
    )

    begin {

    }

    process {
        $params = @{
            CoveMethod = 'EnumerateAccounts'
            Params = @{
                partnerId = $PartnerId ? $PartnerId : $Script:CoveApiSession.PartnerInfo.id
            }
            Id = 'jsonrpc'
        }

        $Data = Invoke-CoveApiRequest @params
        if ($Data) {
            $UnixTimeFields = Get-CoveUnixTimeFields
            if ($Data.Count -gt 1) {
                foreach ($Device in $Data.GetEnumerator()) {
                    foreach ($Property in $Device.psobject.Properties) {
                        if ($Property.Name -in $UnixTimeFields) {
                            $Property.Value = Convert-CoveUnixTime -UnixTime $Property.Value
                        }
                    }
                }
            }
            else {
                foreach ($Property in $Data.psobject.Properties) {
                    if ($Property.Name -in $UnixTimeFields) {
                        $Property.Value = Convert-CoveUnixTime -UnixTime $Property.Value
                    }
                }
            }
            Write-Verbose "Got $($Data.Count) devices"
            if ($DeviceId) {
                # This endpoint does not support API level filtering, so we'll simulate it with the results
                $Data = $Data | Where-Object { $_.Id -eq $DeviceId }
            }
            return $Data
        }
        Write-Verbose "Did not find any devices matching the criteria"
    }

    end {

    }
}
#EndRegion './Public/Get-CoveDevice.ps1' 75
#Region './Public/Get-CoveDeviceStatistic.ps1' -1

function Get-CoveDeviceStatistic {
    <#
    .SYNOPSIS
        Gets devices from the Cove API
    .DESCRIPTION
        Gets devices from the Cove API, using the credentials stored in the script
    .EXAMPLE
        Get-CoveDeviceStatistic -Verbose
        Gets devices from the Cove API
    .EXAMPLE
        Get-CoveDeviceStatistic -PartnerId 1234 -Verbose
        Gets devices from the Cove API for the partner with ID 1234
    .OUTPUTS
        System.Collections.ArrayList
    #>

    [OutputType([System.Collections.ArrayList])]
    [CmdletBinding()]
    param (
        # The ID of the partner to get devices for
        [Parameter()]
        [int]$PartnerId,
        # Filter by type of account - Must be M365, Servers, or Workstations
        [Parameter()]
        [ValidateSet('M365','Hardware')]
        [string]$BackupType
    )

    begin {

    }

    process {

        $ColumnHeaders = Get-CoveDataMap -DataMap ColumnHeaders
        $DataSources = Get-CoveDataMap -DataMap DataSources
        $StatsFields = Get-CoveDataMap -DataMap StatsFields
        $StatusMap = Get-CoveBackupStatusMap

        $Filter = ''
        if ($BackupType) {
            switch ($BackupType) {
                'M365' {
                    $TypeID = 2
                }
                'Hardware' {
                    $TypeID = 1
                }
                default {
                }
            }
            $Filter = "$($ColumnHeaders | Where-Object {$_.Value -eq 'Account Type'} | Select-Object -ExpandProperty Key) == $($TypeID)"
        }

        $Columns = @(
            foreach ($Column in $ColumnHeaders.GetEnumerator()) {
                $Column.Key
            }
            foreach ($Source in $DataSources.GetEnumerator()) {
                foreach ($Field in $StatsFields.GetEnumerator()) {
                    "$($Source.Key)$($Field.Key)"
                }
            }
        )

        $params = @{
            CoveMethod = 'EnumerateAccountStatistics'
            Params = @{
                query = @{
                    PartnerId = $PartnerId ? $PartnerId : $Script:CoveApiSession.PartnerInfo.id
                    RecordsCount = 1000
                    SelectionMode = 'Merged'
                    StartRecordNumber = 0
                    Totals = @()
                    Filter = $Filter
                    Columns = $Columns
                    OrderBy = "$(Get-CoveDataMap -FieldName 'Company Name') ASC"
                }
            }
            Id = 'jsonrpc'
        }



        Write-Verbose "Getting devices statatistics for Partner '$($params.Params.query.PartnerId)'"

        $Data = Invoke-CoveApiRequest @params
        if ($Data) {
            $UnixTimeFields = Get-CoveUnixTimeFields
            $DeviceStats = [System.Collections.ArrayList]@()
            foreach ($Statistic in $Data) {
                $DeviceStat = [PSCustomObject]@{
                    PartnerId = $Statistic.PartnerId
                    AccountId = $Statistic.AccountId
                }
                foreach ($Setting in $Statistic.Settings.GetEnumerator()) {
                    Write-Debug "Processing setting $($Setting.Key)"
                    foreach ($Property in $Setting.psobject.Properties) {
                        Write-Debug "- Processing property $($Property.Name)"
                        $NameSubstring = $Property.Name.Length -gt 3 ? $Property.Name.Substring($Property.Name.Length - 3) : $null
                        if ($Property.Name -is [string] -and ($Property.Name -in $UnixTimeFields -or ($NameSubstring -in $UnixTimeFields))) {
                            $Value = Convert-CoveUnixTime -UnixTime $Property.Value
                        }
                        else {
                            switch ($Property.Name) {
                                'I78' { # Data sources new column code
                                    $Sources =  $($Setting.Value -split 'D' | Where-Object { $_ -ne '' })
                                    $Value = foreach ($Source in $Sources) {
                                        $DataSources.GetEnumerator() | Where-Object { $_.Key -eq "D$Source" } | Select-Object -ExpandProperty Value
                                    }
                                }
                                default {
                                    $Value = $Property.Value
                                }
                            }
                        }
                        try {
                            Write-Debug " - Getting column name for $($Property.Name) from column headers"
                            $ColumnName = $ColumnHeaders.GetEnumerator() | Where-Object { $_.Key -eq $Property.Name } | Select-Object -ExpandProperty Value
                        }
                        catch {
                            $ColumnName = $null
                        }
                        if (!$ColumnName) {
                            $Keys = $Property.Name -split 'F'
                            $Keys[1] = "F$($Keys[1])"
                            $Source = $DataSources.GetEnumerator() | Where-Object { $_.Key -eq $Keys[0] } | Select-Object -ExpandProperty Value
                            $Field = $StatsFields.GetEnumerator() | Where-Object { $_.Key -eq $Keys[1] } | Select-Object -ExpandProperty Value
                            if (-not ($DeviceStat | Get-Member $Source)) {
                                Write-Debug " - Creating new object for $Source with $Field"
                                # add a new pscustomobject to the device stat object
                                $DeviceStat | Add-Member -MemberType NoteProperty -Name $Source -Value ([PSCustomObject]@{})
                            }
                            if ($Field -like '* Session Status') {
                                if ($Value -in $StatusMap.Keys) {
                                    $Value = $StatusMap.[int]$Value
                                }
                            }
                            Write-Debug " - Adding new note to $Source for $Field"
                            $DeviceStat.$Source | Add-Member -MemberType NoteProperty -Name $Field -Value $Value
                            continue
                        }
                        $ColumnName = $ColumnName ? $ColumnName : $Property.Name
                        Write-Debug " Column name for $($Property.Name) is $ColumnName"
                        $DeviceStat | Add-Member -MemberType NoteProperty -Name $ColumnName -Value $Value
                    }
                }
                $DeviceStats.Add($DeviceStat) | Out-Null
            }

            return $DeviceStats

        }
        return $null
    }

    end {

    }
}
#EndRegion './Public/Get-CoveDeviceStatistic.ps1' 160
#Region './Public/Get-CovePartnerInfo.ps1' -1

function Get-CovePartnerInfo {
    <#
    .SYNOPSIS
        Gets the partner information for the Cove API
    .DESCRIPTION
        Gets the partner information for the Cove API, using the credentials stored in the script
    .EXAMPLE
        Get-CovePartnerInfo -Verbose
        Gets the partner information for the Cove API
    .OUTPUTS
        System.Management.Automation.PSCustomObject
    #>

    [CmdletBinding()]
    param (

    )

    begin {

    }

    process {
        $params = @{
            CoveMethod = 'GetPartnerInfo'
            Params = @{
                name = $Script:CoveApiCredentials.Partner
            }
        }
        $Data = Invoke-CoveApiRequest @params
        if ($Data) {
            foreach ($Property in $Data.psobject.Properties) {
                if ($Property.Name -in $UnixTimeFields) {
                    $Property.Value = Convert-CoveUnixTime -UnixTime $Property.Value
                }
            }
            return $Data
        }
        Throw "Failed to get partner info"
    }

    end {

    }
}
#EndRegion './Public/Get-CovePartnerInfo.ps1' 45
#Region './Public/New-CoveApiCredential.ps1' -1

function New-CoveApiCredential {
    <#
    .SYNOPSIS
        Sets the credentials for the Cove API
    .DESCRIPTION
        Stores the attributes required by the Cove API to authenticate
    .PARAMETER User
        The username of the API user provided by Cove
    .PARAMETER Password
        The password of the user provided by Cove
    .PARAMETER Partner
        The partner name displayed in the Cove portal, with the format "PartnerName (admin@partnerdomain.tld)"
    .PARAMETER Url
        The URL for the Cove API (default: https://api.backup.management/jsonapi)
    .EXAMPLE
        $creds = @{
            User = 'username@domain.tld'
            Password = 'supersecurepassword' | ConvertTo-SecureString -AsPlainText -Force
            Partner = 'PartnerName (admin@partnerdomain.tld)'
        }
        New-CoveApiCredential @creds
        Stores the required attributes for the Cove API to use in future calls
    .EXAMPLE
        $creds = @{
            User = 'username@domain.tld'
            Password = 'supersecurepassword' | ConvertTo-SecureString -AsPlainText -Force
            Partner = 'PartnerName (admin@partnerdomain.tld)'
            Url = 'https://api.backup.management/jsonapi'
        }
        New-CoveApiCredential @creds
        Overrides the default URL for the Cove API and stores the required attributes for the Cove API to use in future calls
    .OUTPUTS
        None

    #>



    [CmdletBinding(SupportsShouldProcess)]
    param (
        # Username for the API
        [Parameter(Mandatory = $true)]
        [string]$User,
        # Password for the API
        [Parameter(Mandatory = $true)]
        [securestring]$Password,
        # Partner name displayed in the Cove portal
        [Parameter(Mandatory = $true)]
        [string]$Partner,
        # URL for the Cove API (default: https://api.backup.management/jsonapi)
        [Parameter()]
        [string]$Url = "https://api.backup.management/jsonapi"
    )


    begin {

    }

    process {
        if (!$User) {
            $User = Read-Host -Prompt "Enter the username for the Cove API"
        }
        if (!$Password) {
            $Password = Read-Host -Prompt "Enter the password for the Cove API" -AsSecureString
        }
        if (!$Partner) {
            $Partner = Read-Host -Prompt "Enter the partner name displayed in the Cove portal"
        }

        $Script:creds = @{
            User = $User
            Password = $Password
            Partner = $Partner
            Url = $Url
        }

        Set-Variable -Name CoveApiCredentials -Value $script:creds -Scope Script -Visibility Private -Force

    }

    end {
        Remove-Variable -Name creds -Scope Script -Force
    }
}
#EndRegion './Public/New-CoveApiCredential.ps1' 85
#Region './Public/New-CoveApiSession.ps1' -1

function New-CoveApiSession {
    <#
    .SYNOPSIS
        Initiates a login session with the Cove API
    .DESCRIPTION
        Checks for an existing visa and if none is found, initiates a login session with the Cove API
    .EXAMPLE
        New-CoveApiSession -Verbose
        Initiates a login session with the Cove API and outputs the result to the console
    .OUTPUTS
        None
    #>



    [CmdletBinding(SupportsShouldProcess)]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingUsernameAndPasswordParams', '')]
    param (

    )

    begin {
    }

    process {
        # if the debug flag is set
        if ($PSDebugContext) {
            Write-Debug "Debug flag set, purging existing session"
            Remove-Variable -Name CoveApiSession -Scope Script -Force
            return
        }
        if (Test-CoveApiVisa) {
            Write-Verbose "Visa found, no need to login"
            return
        }
        if (!$Script:CoveApiCredentials) {
            Write-Output "No credentials found, please run New-CoveApiCredential to set the credentials for Cove API"
            return $null
        }


        $params = @{
            ContentType = 'application/json'
            Method = 'POST'
            Body = @{
                jsonrpc = '2.0'
                method = 'Login'
                id = 2
                params = @{
                    username = $Script:CoveApiCredentials.User
                    password = (New-Object PSCredential 'user', $Script:CoveApiCredentials.Password).GetNetworkCredential().Password
                    partner = $Script:CoveApiCredentials.Partner
                }
            } | ConvertTo-Json
            Uri = $Script:CoveApiCredentials.Url
            UseBasicParsing = $true
            SessionVariable = 'CoveSession'
        }

        try {
            $Request = Invoke-WebRequest @params
        }
        catch {
            Throw "Failed to login to Cove API: $($_.Exception.Message)"
        }

        New-Variable -Name CoveApiSession -Scope Script -Visibility Private -Force -Value @{}

        try {
            $Response = $Request | ConvertFrom-Json
            $Script:CoveApiSession.cookies = $CoveSession.Cookies.GetCookies($Script:CoveApiCredentials.Url)
            $Script:CoveApiSession.visa = $Response.visa
            $Script:CoveApiSession.userid = $Response.result.result.userid
        }
        catch {
            Throw "Failed to parse Cove API response: $($_.Exception.Message)"
        }
        if ($Response.error) {
            Throw "Failed to login to Cove API: $($Response.error.message)"
        }
        if (!$Script:CoveApiSession.visa) {
            Throw "Failed to login to Cove API: No visa returned"
        }

        # Set the expiry time for the visa
        $Script:CoveApiSession.validfrom = (Get-Date -Date "1970-01-01 00:00:00Z").ToUniversalTime().AddSeconds($Script:CoveApiSession.visa.split('-')[3])

        Write-Verbose "Login successful, visa valid from $($Script:CoveApiSession.validfrom)"

        try {
            $Script:CoveApiSession.PartnerInfo = Get-CovePartnerInfo
            Write-Verbose "Partner $($Script:CoveApiSession.PartnerInfo.name) logged in with ID $($Script:CoveApiSession.PartnerInfo.id)"
        }
        catch {
            Throw "Failed to get partner info: $($_.Exception.Message)"
        }

    }

    end {

    }
}
#EndRegion './Public/New-CoveApiSession.ps1' 103