public/Organizations.ps1

# Meraki Organization functions
using namespace System.Collections.Generic

function Set-MerakiAPI() {
    [CmdletBinding()]
    Param(
        [string]$APIKey,
        [string]$OrgID,
        [string]$ProfileName
    )
    
    $configPath = "{0}/.meraki" -f $HOME
    $configFile = "{0}/config.json" -f $configPath

    if (-not (Test-Path -Path $configFile)) {
        if (-not $APIKey) {
            $APIKey = Read-Host -Prompt "API Key: "
        }   

        if (-not $APIKey) {
            Throw "APIKey required if config file does not exist!"
        }
        if ((-not $OrgId) -and (-not $profileName)) {
            $orgs = Get-MerakiOrganizations -APIKey $APIKey
            $config = @{
                APIKey = $apiKey
            }
            $config.Add('profiles', @{default = $orgs[0].Id})
            foreach ($org in $orgs) {
                $config.profiles.Add($org.name, $org.id)
            }
        } else {
            if (-not $profileName) {
                $config = @{
                    APIKey = $APIKey
                }
                $config.Add('profiles',@{default = $OrgID})
            } else {
                $config = @{
                    APIKey = $apiKey
                }
                $config.Add('profiles',@{})
                $config.profiles.Add($profileName, $OrgID)
            }
        }
    } else {
        # Read config into an object
        $oConfig = Get-Content -Raw -Path $configFile | ConvertFrom-Json
        # Convert the object to a hash table
        $config = $oConfig | ConvertTo-HashTable

        if ($APIKey) {
            If ($config.APIKey -ne $APIKey) {
                Write-Host "The APIKey you entered does not match the APIKey in the config file. This will overwrite the existing config file!" -ForegroundColor Yellow
                $response = ($R = read-host "Continue? [Y/n]:") ? $R : 'Y'
                if ($response-eq "Y") {
                    $config = @{
                        APIKey = $APiKey
                    }
                } else {
                    Throw "Aborting!"
                }
            }
        }
        if ((-not $OrgID) -and (-not $profileName)) {
            $response = ($R = read-host "Overwrite Profiles from organization names? [Y/n]:") ? $R : 'Y'
            if ($response -eq 'Y') {
                $orgs = Get-MerakiOrganizations -APIKey $config.APIKey
                $config.Add('profiles', @{default = $orgs[0].Id})
                foreach ($org in $orgs) {
                    $config.profiles.Add($org.name, $org.Id)
                }
            }
        } else {
            if (-not $profileName) {
                if ($config.profiles.default) {
                    $config.profiles.default = $orgId
                } else {
                    $config.profiles.Add('default', $orgId)
                }
            } else {
                if ($config.profiles.$profileName) {
                    $config.profiles.$profileName = $OrgId
                } else {
                    $config.profiles.Add($profileName, $OrgId)
                }
            }
        }
    }
    
    if (-not (Test-Path -Path $configPath)) {
        [void](New-Item -Path $configPath -ItemType:Directory)
    }

    $config | ConvertTo-Json | Out-File -FilePath $configFile
    <#
    .SYNOPSIS
    Set the configuration file.
    .DESCRIPTION
    Sets up the configuration file. This can be the initial configuration or creating named profiles.
    .PARAMETER APIKey
    Your Meraki API key. If a configuration file exists and this key does not match the key in the file a
    new file will be created overwriting the existing file.
    .PARAMETER OrgID
    The ID of an organization to add to the profile.
    .PARAMETER profileName
    The name of the profile to create. If omitted the OrgID is set as the default profile.
    .NOTES
    If the OrgID and profileName parameters are omitted named profiles will be created based on the Organization names pulled from Meraki.
    This approach may not be the best as most of the time these names will have multiple words and spaces and just be too long.
    .EXAMPLE
    Create the default profile
    PS> Set-MerakiAPI -APIKey 'GDTE63534HD74BD93847' -OrgId 123456
    .EXAMPLE
    Create a Named Profile.
    Set-MerakiAPI -OrgId 123456 -ProfileName USNetwork
    #>

}

function Set-MerakiProfile () {
    Param(
        [Parameter(Mandatory = $true)]
        [string]$profileName
    )
    $configFile = "{0}/.meraki/config.json" -f $home
    $Config = Get-Content -Path $configFile | ConvertFrom-Json | ConvertTo-HashTable

    $orgID = $Config.profiles.$profileName
    if (-not $OrgId) {
        throw "Invalid profile name!"
    }
    Set-MerakiAPI -OrgID $orgID -profileName 'default'
    <#
    .SYNOPSIS
    Set the default profile to the specified named profile.
    .DESCRIPTION
    Use this function to set the default profile for all subsequent command.
    This changes the organization ID of the default profile so any future commands will use this profile even after closing out of PowerShell.
    .PARAMETER profileName
    The name of the profile to use.
    #>

}


function Get-MerakiOrganizations() {
    Param(
        [string]$APIKey
    )

    $Uri = "{0}/organizations" -f $BaseURI
    
    If ($APIKey) {
        $Headers = @{
            "X-Cisco-Meraki-API-Key" = $APIKey
            "Content-Type" = 'application/json'
        }
    } else {
        $Headers = Get-Headers
    }

    try {
        $response = Invoke-RestMethod -Method GET -Uri $Uri -Headers $Headers -PreserveAuthorizationOnRedirect
        
        return $response
    } catch {
        throw $_
    }
    <#
    .SYNOPSIS
    Get Meraki Organizations
    .DESCRIPTION
    Get all Meraki Organizations your API Key has access to.
    .PARAMETER APIKey
    Meraki API Key.
    #>

}

Set-Alias -Name GMOrgs -Value Get-MerakiOrganizations -Option ReadOnly

function Get-MerakiOrganization() {
    [CmdLetBinding(DefaultParameterSetName = 'default')]
    Param (
        [Parameter(ParameterSetName = 'org')]
        [string]$OrgID,
        [Parameter(ParameterSetName = 'profile')]
        [string]$profileName
    )
<#
    If ($OrgId -and $profileName) {
        Write-Host "The parameters OrgId and ProfileName cannot be used together!" -ForegroundColor Red
        return
    }
 #>

    if (-not $OrgID) {
        $config = Read-Config
        if ($profileName) {
            $OrgId = $config.profiles.$profileName
            if (-not $OrgId) {
                throw "Invalid profile name!"
            }
        } else {
            $OrgId = $config.profiles.default
            if (-not $OrgId) {
                throw "There is no default profile. You must use the -OrgId parameter and supply the Organization Id."
            }
        }
    }
    $Uri = "{0}/organizations/{1}" -f $BaseURI, $OrgId
    $Headers = Get-Headers

    try {
        $response = Invoke-RestMethod -Method GET -Uri $Uri -Headers $Headers -PreserveAuthorizationOnRedirect

        return $response
    } catch {
        throw $_
    }
    <#
    .SYNOPSIS
    Get Meraki Organization
    .PARAMETER OrgId
    The organization ID
    .PARAMETER profileName
    Use the profile name to get organization.
    .OUTPUTS
    A Meraki organization Object
    #>

}

Set-Alias -Name GMOrg -value Get-MerakiOrganization -Option ReadOnly

function New-MerakiOrganization() {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory = $true)]
        [string]$Name,
        [string]$ManagementName,
        [string]$ManagementValue
    )

    $Headers = Get-Headers

    $Uri = "{0}/organizations" -f $BaseURI

    $_Body = @{
        name = $Name
    }

    If ($ManagementName) {
        $_Body.Add("management", @{
            details = @(
                @{
                    "name" = $ManagementName
                    "value" = $
                }
            )
        })
    }
    $body = $_Body | ConvertTo-Json -Depth 5 -Compress

    try {
        $response = Invoke-RestMethod -Method POST -Uri $Uri -Headers $Headers -Body $body -PreserveAuthorizationOnRedirect
        return $response
    }
    catch {
        throw $_
    }
    <#
    .SYNOPSIS
    Create an organization
    .DESCRIPTION
    Create a new Meraki Organization
    .PARAMETER Name
    The name of the organization
    .PARAMETER ManagementName
    Name of the management system
    .PARAMETER ManagementValue
    Value of the management system
    #>

}

function Set-MerakiOrganization() {
    [CmdletBinding(DefaultParameterSetName = 'default')]
    Param(
        [Parameter(Mandatory = $true)]
        [string]$Name,
        [string]$ManagementName,
        [string]$ManagementValue,
        [switch]$ApiEnabled,
        [Parameter(ParameterSetName = 'org')]
        [string]$OrgID,
        [Parameter(ParameterSetName = 'profile')]
        [string]$profileName
    )
<#
    If ($OrgId -and $profileName) {
        Write-Host "The parameters OrgId and ProfileName cannot be used together!" -ForegroundColor Red
        return
    }
 #>

    if (-not $orgID) {
        $config = Read-Config
        if ($profileName) {
            $orgID = $config.profiles.$profileName
            if (-not $orgID) {
                throw "Invalid profile name!"
            }
        } else {
            $orgID = $config.profiles.default
        }
    }
    $Uri = "{0}/organizations/{1}" -f $BaseURI, $orgID

    $Headers = Get-Headers

    $_Body = @{
        name = $Name        
    }
    if ($ManagementName) {
        $_Body.Add("management", @{
            details = @(
                @{
                    name = $ManagementName
                    value = $ManagementValue
                }
            )
        })        
    }
    if ($ApiEnabled) {
        $_Body.Add("api", @{
            "enabled" = $ApiEnabled.IsPresent
        })
    }

    $body = $_Body | ConvertTo-Json -Depth 5 -Compress

    try {
        $response = Invoke-RestMethod -Method PUT -Uri $Uri -Headers $Headers -Body $body -PreserveAuthorizationOnRedirect
        return $response
    }
    catch {
        throw $_
    }
    <#
    .SYNOPSIS
    Update an organization
    .DESCRIPTION
    Update a Meraki Organization
    .PARAMETER OrgID
    The organization Id
    .PARAMETER profileName
    The saved profile name
    .PARAMETER Name
    The name of the organization
    .PARAMETER ManagementName
    Name of the management data
    .PARAMETER ManagementValue
    Value of the management data
    .PARAMETER ApiEnabled
    If present, enable the access to the Cisco Meraki Dashboard API
    .OUTPUTS
    A Meraki organization object
    #>

}

#region Organization Networks
function Get-MerakiNetworks() {
    [CmdletBinding(DefaultParameterSetName = 'default')]
    Param(
        [string]$ConfigTemplateId,
        [switch]$IsBoundToConfigTemplate,
        [switch]$IncludeTemplates,
        [Parameter(ParameterSetName = 'org')]
        [string]$OrgID,
        [Parameter(ParameterSetName = 'profile')]
        [string]$profileName
    )

<#
    If ($OrgId -and $profileName) {
        Write-Host "The parameters OrgId and ProfileName cannot be used together!" -ForegroundColor Red
        return
    }
 #>

    if (-not $orgID) {
        $config = Read-Config
        if ($profileName) {
            $orgID = $config.profiles.$profileName
            if (-not $orgID) {
                throw "Invalid profile name!"
            }
        } else {
            $orgID = $config.profiles.default
        }
    }
    $Uri = "{0}/organizations/{1}/networks" -f $BaseURI, $orgID
    if ($ConfigTemplateId){
        $Uri = "{0}?configTemplateId=" -f $Uri, $ConfigTemplateId
    }

    If ($IsBoundToConfigTemplate.IsPresent) {
        if ($Uri.Contains("?")) {
            $Uri = "{0}&isBoundToConfigTemplate=true" -f $Uri
        } else {
            $Uri = "{0}?isBoundToConfigTemplate=false" -f $Uri
        }
    }

    $Headers = Get-Headers
    try {
        $response = Invoke-RestMethod -Method GET -Uri $Uri -Headers $Headers -PreserveAuthorizationOnRedirect
        If ($IncludeTemplates.IsPresent) {
            $templates = @{}
            Get-MerakiOrganizationConfigTemplates | ForEach-Object {
                $templates.Add($_.id, $_)
            }
            foreach ($network in $response) {
                If ($Network.isBoundToConfigTemplate) {
                    $template = $templates[$Network.configTemplateId]
                    $response[$response.IndexOf($Network)] | Add-Member -MemberType NoteProperty -Name "configTemplate" -Value $template
                }
            }
        } 
        return $response
    } catch {
        throw $_
    }
    <#
    .SYNOPSIS
    Get all Meraki Networks.
    .DESCRIPTION
    Get all Meraki networks in an organization.
    .PARAMETER OrgID
    The Organization ID.
    .PARAMETER profileName
    The profile name to use to get networks.
    .PARAMETER IncludeTemplates
    Includes a configTemplate property containing the configuration template.
    .PARAMETER ConfigTemplateId
    Get all networks bound to this template Id.
    .PARAMETER IsBoundToConfigTemplate
    Get only networks bound to config templates.
    .OUTPUTS
    An array of Meraki network objects.
    #>

}

Set-Alias -Name GMNets -Value Get-MerakiNetworks -Option ReadOnly

function Add-MerakiNetwork() {
    [CmdletBinding(DefaultParameterSetName = 'default')]
    Param(
        [Parameter(Mandatory = $true)]
        [string]$Name,
        [Parameter(Mandatory = $true)]
        [string[]]$ProductTypes,
        [string]$TimeZone,
        [string]$Notes,
        [string[]]$Tags,
        [string]$CopyFromNetworkId, 
        [Parameter(ParameterSetName = 'org')]       
        [string]$OrgID,
        [Parameter(ParameterSetName = 'profile')]
        [string]$profileName
    )
<#
    If ($OrgId -and $profileName) {
        Write-Host "The parameters OrgId and ProfileName cannot be used together!" -ForegroundColor Red
        return
    }
 #>

    if (-not $orgID) {
        $config = Read-Config
        if ($profileName) {
            $orgID = $config.profiles.$profileName
            if (-not $orgID) {
                throw "Invalid profile name!"
            }
        } else {
            $orgID = $config.profiles.default
        }
    }

    $Headers = Get-Headers
    $Uri = "{0}/organizations/{1}/networks"

    $_Body = @{
        name = $Name
        productTypes = $ProductTypes
    }
    if ($TimeZone) { $_Body.Add("timeZone", $TimeZone) }
    if ($Notes) { $_Body.Add("notes", $Notes) }
    if ($Tags) { $_Body.Add("tags", $Tags) }
    if ($CopyFromNetworkId) { $_Body.Add("copyFromNetworkId", $CopyFromNetworkId) }

    $body = $_Body | ConvertTo-Json -Depth 5 -Compress

    try {
        $response = Invoke-RestMethod -Method POST -Uri $Uri -Headers $Headers -Body $body -PreserveAuthorizationOnRedirect
        return $response        
    } catch {
        throw $_
    }
    <#
    .SYNOPSIS
    Create a network
    .DESCRIPTION
    Create a network in an organization
    .PARAMETER Name
    The name of the network
    .PARAMETER ProductTypes
    The product type(s) of the new network. If more than one type is included, the network will be a combined network.
    .PARAMETER TimeZone
    Time zone name from the ICANN tz database
    .PARAMETER Notes
    Add any notes or additional information about this network here.
    .PARAMETER Tags
    A list of tags to be applied to the network
    .PARAMETER CopyFromNetworkId
    The ID of the network to copy configuration from. Other provided parameters will override the copied configuration, except type which must match this network's type exactly.
    .PARAMETER OrgId
    Optional Organizational ID. if this parameter is not provided the default Organization ID will be retrieved from the settings file.
    If this parameter is provided it will override the default Organization ID in the settings file.
    .OUTPUTS
    An object containing the new network.
    #>

}

#endregion

function Get-MerakiOrganizationConfigTemplates() {
    [CmdletBinding(DefaultParameterSetName = 'default')]
    Param(
        [Parameter(ParameterSetName = 'org')]
        [string]$OrgID,
        [Parameter(ParameterSetName = 'default')]
        [string]$profileName
    )
<#
    If ($OrgId -and $profileName) {
        Write-Host "The parameters OrgId and ProfileName cannot be used together!" -ForegroundColor Red
        return
    } #>

    
    if (-not $OrgID) {
        $config = Read-Config
        if ($profileName) {
            $OrgID = $config.profiles.$profileName
            if (-not $OrgID) {
                throw "Invalid profile name!"
            }
        } else {
            $OrgID = $config.profiles.default
        }
    }

    $Uri = "{0}/organizations/{1}/configTemplates" -f $BaseURI, $OrgID
    $headers = Get-Headers

    try{
        $response = Invoke-RestMethod -Method GET -Uri $Uri -Headers $headers -PreserveAuthorizationOnRedirect

        return $response
    } catch {
        throw $_
    }
    <#
    .SYNOPSIS
    Get the Organization Configuration Templates
    .DESCRIPTION
    Get the configuration templates for a given organization.
    .PARAMETER OrgID
    The Organization Id. If omitted uses the default profile.
    .PARAMETER profileName
    The profile name to use to get the templates.
    .OUTPUTS
    An array of Meraki configuration template objects.
    #>

}

Set-Alias -Name GMOrgTemplates -value Get-MerakiOrganizationConfigTemplates -Option ReadOnly

function Get-MerakiOrganizationConfigTemplate () {
    [CmdletBinding(DefaultParameterSetName = 'default')]
    Param(
        [Parameter(
            Mandatory,
            ValueFromPipelineByPropertyName
        )]
        [Alias('TemplateId')]
        [string]$Id,
        [Parameter(ParameterSetName = 'org')]
        [string]$OrgID,
        [Parameter(ParameterSetName = 'profile')]
        [string]$profileName
    )

    Begin {
        If ($OrgId -and $profileName) {
            Write-Host "The parameters OrgId and ProfileName cannot be used together!" -ForegroundColor Red
            return
        }

        if (-not $OrgID) {
            $config = Read-Config
            if ($profileName) {
                $OrgID = $config.profiles.$profileName
                if (-not $OrgID) {
                    throw "Invalid profile name!"
                }
            } else {
                $OrgID = $config.profiles.default
            }
        }

        $Headers = Get-Headers
    }

    Process {
        $Uri = "{0}/organizations/{1}/configTemplates/{2}" -f $BaseURI, $OrgID, $Id

        try {
            $response = Invoke-RestMethod -Method GET -Uri $Uri -Headers $Headers -PreserveAuthorizationOnRedirect
            return $response
        }
        catch {
            throw $_
        }
    }
    <#
    .SYNOPSIS
    Retrieve a configuration Template
    .DESCRIPTION
    Retrieves the configuration template designated by the provided template Id.
    .PARAMETER Id
    The ID of the template to retrieve.
    .PARAMETER OrgID
    The Organization ID. Of omitted this is puled from the default configuration.
    .PARAMETER profileName
    The saved profile name to use. Cannot be used ith the OrgId parameter.
    .OUTPUTS
    A configuration template object.
    #>

}

function Get-MerakiOrganizationDevices() {
    [CmdletBinding(DefaultParameterSetName = 'default')]
    Param(
        [Parameter(ParameterSetName = 'org')]
        [string]$OrgID,
        [Parameter(ParameterSetName = 'profile')]
        [string]$profileName
    )
<#
    If ($OrgId -and $profileName) {
        Write-Host "The parameters OrgId and ProfileName cannot be used together!" -ForegroundColor Red
        return
    }
 #>

    If (-not $OrgID) {
        $config = Read-Config
        if ($profileName) {
            $OrgID = $config.profiles.$profileName
            if (-not $OrgID) {
                throw "Invalid profile name!"
            }
        } else {
            $OrgID = $config.profiles.default
        }
    }

    $Uri = "{0}/organizations/{1}/devices" -f $BaseURI, $OrgID
    $Headers = Get-Headers

    try {
        $response = Invoke-RestMethod -Method GET -Uri $Uri -Headers $Headers -PreserveAuthorizationOnRedirect

        return $response
    } catch {
        throw $_
    }
    <#
    .SYNOPSIS
    Get organization Devices.
    .DESCRIPTION
    Get all devices in an organization.
    .PARAMETER OrgID
    The Organization Id. If omitted uses the default profile.
    .PARAMETER profileName
    Profile name to use to get the devices. If omitted uses the default profile.
    .OUTPUTS
    AN array of Meraki Device objects.
    #>

}

Set-Alias GMOrgDevs -Value Get-MerakiOrganizationDevices -Option ReadOnly

function Get-MerakiOrganizationAdmins() {
    [CmdletBinding(DefaultParameterSetName = 'default')]
    Param(
        [Parameter(ParameterSetName = 'org')]
        [string]$OrgID,
        [Parameter(ParameterSetName = 'profile')]
        [string]$profileName
    )
<#
    If ($OrgId -and $profileName) {
        Write-Host "The parameters OrgId and ProfileName cannot be used together!" -ForegroundColor Red
        return
    }
 #>
    
    If (-not $orgID) {
        $config = Read-Config
        if ($profileName) {
            $OrgID = $config.profiles.$profileName
            if (-noy $OrgID) { 
                throw "Invalid profile name!"
            }
        } else {
            $OrgID = $config.profiles.default
        }
    }

    $Uri = "{0}/organizations/{1}/admins" -f $BaseURI, $OrgID
    $Headers = Get-Headers

    try {
        $response = Invoke-RestMethod -Method GET -Uri $uri -Headers $Headers -PreserveAuthorizationOnRedirect

        return $response
    }catch {
        throw $_
    }
    <#
    .SYNOPSIS
    Get Organization Admins.
    .PARAMETER OrgID
    The Organization ID. If omitted uses the default profile.
    .PARAMETER profileName
    The profile name to get admins with. If omitted used the default profile.
    .OUTPUTS
    An array of Meraki Admin objects.
    #>

}

Set-Alias -name GMOrgAdmins -Value Get-MerakiOrganizationAdmins -Option ReadOnly


function Get-MerakiOrganizationConfigurationChanges() {
    [CmdletBinding(DefaultParameterSetName='default')]
    Param(       
        [Parameter(ParameterSetName = 'dates')]
        [Parameter(ParameterSetName = 'datesWithOrg')]
        [Parameter(ParameterSetName = 'datesWithProfile')]
        [ValidateScript({$_ -is [DateTime]})]
        [Alias('StartTime')]
        [datetime]$StartDate,

        [Parameter(ParameterSetName = 'dates', Mandatory)]
        [Parameter(ParameterSetName = 'datesWithOrg', Mandatory)]
        [Parameter(ParameterSetName = 'datesWithProfile', Mandatory)]
        [ValidateScript({$_ -is [DateTime]})]
        [Alias('EndTime')]
        [DateTime]$EndDate,

        [Parameter(ParameterSetName = 'days', Mandatory)]
        [Parameter(ParameterSetName = 'daysWithOrg', Mandatory)]
        [Parameter(ParameterSetName = 'daysWithProfile', Mandatory)]
        [ValidateScript({$_ -is [int32]})]
        [Alias('TimeSpan')]
        [ValidateRange(0,31)]
        [Int]$Days,

        [ValidateScript({$_ -is [int]})]
        [ValidateRange(1,1000)]
        [int]$PerPage,

        [ValidateScript({$_ -is [int]})]
        [ValidateRange(0,1000)]
        [int]$Pages = 1,
        
        [string]$NetworkID,
        
        [string]$AdminID,
        
        [Parameter(ParameterSetName = 'org', Mandatory)]
        [Parameter(ParameterSetName = 'daysWithOrg', Mandatory)]
        [Parameter(ParameterSetName = 'datesWithOrg', Mandatory)]
        [string]$OrgID,

        [Parameter(ParameterSetName = 'profile', Mandatory)]
        [Parameter(ParameterSetName = 'daysWithProfile', Mandatory)]
        [Parameter(ParameterSetName = 'datesWithProfile', Mandatory)]
        [string]$ProfileName
    )

    If (-not $OrgID) {
        $config = Read-Config
        if ($profileName) {
            $OrgId = $config.profiles.$profileName
            if (-not $OrgID) {
                throw "Invalid profile name!"
            }
        } else {
            $OrgID = $config.profiles.default
        }
    }

    $Results = [List[PsObject]]::New()

    $Uri = "{0}/organizations/{1}/configurationChanges" -f $BaseURI, $OrgID
    $Headers = Get-Headers

    $psBody = @{}
    if ($StartTime) {
        $T0 = "{0:s}" -f $StartTime
        $psBody.Add("t0", $T0)
    }

    if ($EndTime) {
        $T1 = "{0:s}" -f $EndTime
        $psBody.add("t1", $T1)
    }

    if ($PerPage) {
        $psBody.Add("perPage", $PerPage)
    }

    if ($NetworkID) {
        $psBody.Add("networkId", $NetworkID)
    }

    if ($AdminID) {
        $psBody.Add("adminId", $AdminID)
    }

    $Body = $psBody | ConvertTo-Json

    try {
        $response = Invoke-WebRequest -Method GET -Uri $Uri -body $Body -Headers $Headers -PreserveAuthorizationOnRedirect
        [List[PsObject]]$result = $response.Content | ConvertFrom-Json
        if ($result) {
            $Results.AddRange($result)
        }
        $page = 1
        if ($Pages -ne 1) {
            $done = $false
            do {
                if ($response.RelationLink['next']) {
                    $Uri = $response.RelationLink['next']
                    $response = Invoke-WebRequest -Method GET -Uri $Uri -Headers $Headers
                    [List[PsObject]]$result = $response.Content | ConvertFrom-Json
                    if ($result) {
                        $Results.AddRange($result)
                    }
                    $page += 1
                    if ($page -gt $Pages) {
                        $done = $true
                    }
                } else {
                    $done = $true
                }
            } until ($done)
        }

        return $Result.ToArray()
     } catch {
        throw $_
     }
    <#
    .SYNOPSIS
    Get Organization Configuration Changes
    .DESCRIPTION
    Gets configuration changes made to an organization's network.
    .PARAMETER OrgID
    The Organization Id. If omitted used th default profile.
    .PARAMETER profileName
    The profile name to use to get the changes. If omitted used th default profile.
    .PARAMETER StartTime
    The start time to pull changes.
    .PARAMETER EndTime
    The end time to pull changes.
    .PARAMETER Days
    Number of days to pull changes
    .PARAMETER PerPage
    Number of records to pull per page.
    .PARAMETER Pages
    Number of pages to retrieve. 0 = all pages. Default is 1.
    .PARAMETER NetworkID
    Filter results by Network ID.
    .PARAMETER AdminID
    Filter results by Admin ID.
    .OUTPUTS
    An array of configuration change objects.
    .EXAMPLE
    Filter logs for last 10 days by Administrator.
    PS> Get-MerakiOrganizationAdmins | Where-Object {$_.Name -eq "John Doe"} | Get-MerakiOrganizationConfigurationChanges -TimeSpan 10
    .EXAMPLE
    Filter logs for changes to the Miami network that occurred between 6/1/2020 and 6/30/2020
    Get-MerakiNetworks | Where-Object {$_.Name -like "*Miami*"} | Get-MerakiOrganizationConfigurationChanges -StartTime "06/01/2020" -EndTime "06/30/2020"
    #>

    
}

Set-Alias -name GMOrgCC -Value Get-MerakiOrganizationConfigurationChanges -Option ReadOnly


function Get-MerakiOrganizationThirdPartyVpnPeers() {
    [CmdletBinding(DefaultParameterSetName = 'default')]
    Param(
        [Parameter(ParameterSetName = 'org')]
        [string]$OrgID,
        [Parameter(ParameterSetName = 'profile')]
        [string]$profileName
    )
<#
    If ($OrgId -and $profileName) {
        Write-Host "The parameters OrgId and ProfileName cannot be used together!" -ForegroundColor Red
        return
    }
 #>

    if (-not $OrgID) {
        $config = Read-Config
        if ($profileName) {
            $OrgId = $config.profiles.$profileName
            if (-not $OrgID) {
                throw "Invalid profile name!"
            }
        } else {
            $OrgId = $config.profiles.default
        }
    }

    $Uri = "{0}/organizations/{1}/appliance/vpn/thirdPartyVPNPeers" -f $BaseURI, $OrgID
    $Headers = Get-Headers

    try {
        $response = Invoke-RestMethod -Method GET -Uri $Uri -Headers $Headers -PreserveAuthorizationOnRedirect
    
        return $response.peers 
    } catch {
        throw $_
    }
    <#
    .SYNOPSIS
    Get Organization 3rd party VPNs.
    .PARAMETER OrgID
    Organization ID. If omitted used th default profile.
    .PARAMETER profileName
    Profile Name to use. If omitted used th default profile.
    .OUTPUTS
    An array of VPN-peer objects.
    #>

}

Set-Alias -Name GMOrg3pVP -Value Get-MerakiOrganizationThirdPartyVPNPeers -Option ReadOnly

function Set-MerakiOrganizationThirdPartyVpnPeer() {
    [CmdletBinding(DefaultParameterSetName = 'default')]
    Param(
        [Parameter(Mandatory = $true)]
        [string]$Name,
        [Parameter(Mandatory = $true)]
        [string]$Secret,
        [ValidateSet('1', '2')]
        [string]$IkeVersion,
        [ValidateSet('default', 'aws', 'azure')]
        [string]$IpsecPoliciesPreset,
        [string]$LocalId,
        [string]$PublicIp,
        [Parameter(Mandatory = $true)]
        [string[]]$PrivateSubnets,
        [string]$RemoteId,
        [string[]]$NetworkTags = 'all',
        [PSObject]$IpsecPolicies,
        [Parameter(ParameterSetName = 'org')]
        [string]$OrgID,
        [Parameter(ParameterSetName = 'profile')]
        [string]$profileName
    )
<#
    If ($OrgId -and $profileName) {
        Write-Host "The parameters OrgId and ProfileName cannot be used together!" -ForegroundColor Red
        return
    }
 #>

    if (-not $OrgID) {
        $config = Read-Config
        if ($profileName) {
            $OrgId = $config.profiles.$profileName
            if (-not $OrgID) {
                throw "Invalid profile name!"
            }
        } else {
            $OrgId = $config.profiles.default
        }
    }

    $Headers = Get-Headers

    $Uri = "{0}/organizations/{1}/appliance/vpn/thirdPartyVPNPeers" -f $BaseURI, $OrgID

    $Peers = @{}
    Get-MerakiOrganizationThirdPartyVpnPeers | ForEach-Object {
        $Peers.Add($Name, $_)
    }

    if (-not $Peers[$Name]) {
        throw "Peer $Name is not found!"
    }

    if ($IkeVersion) { $Peers[$Name].ikeVersion = $IkeVersion }
    if ($IpsecPoliciesPreset) { $Peers[$Name].IpsecPoliciesPreset = $IpsecPoliciesPreset } 
    if ($LocalId) { $Peers[$Name].localId = $LocalId }
    if ($publicIp) { $Peers[$Name].publicIp = $PublicIp }
    if ($RemoteId) { $Peers[$Name].remoteIp = $RemoteId }
    if ($Secret) { $Peers[$Name].secret = $Secret}
    if ($NetworkTags) { $Peer[$Name].networkTags = $NetworkTags }
    if ($PrivateSubnets) { $Peers[$Name].privateSubnets = $PrivateSubnets }
    if ($IpsecPolicies) { $Peer[$Name].ipsecPolicies = $IpsecPolicies}
        
    $NewPeers = $Peers.Values
    $_Body = @{
        peers = $NewPeers
    }

    $Body = $_Body | ConvertTo-Json -Depth 5 -Compress

    try {
        $response = Invoke-RestMethod -Method PUT -Uri $Uri -Headers $Headers -Body $Body -PreserveAuthorizationOnRedirect

        return $response
    } catch {
        throw $_
    }
}

function New-MerakiOrganizationThirdPartyVpnPeer() {
    [CmdletBinding(DefaultParameterSetName = 'default')]
    Param(
        [Parameter(Mandatory = $true)]
        [string]$Name,
        [Parameter(Mandatory = $true)]
        [string]$Secret,
        [ValidateSet('1', '2')]
        [string]$IkeVersion,
        [ValidateSet('default', 'aws', 'azure')]
        [string]$IpsecPoliciesPreset,
        [string]$LocalId,
        [string]$PublicIp,
        [Parameter(Mandatory = $true)]
        [string[]]$PrivateSubnets,
        [string]$RemoteId,
        [string[]]$NetworkTags = 'all',
        [PSObject]$IpsecPolicies,
        [Parameter(ParameterSetName = 'org')]
        [string]$OrgID,
        [Parameter(ParameterSetName = 'profile')]
        [string]$profileName
    )
<#
    If ($OrgId -and $profileName) {
        Write-Host "The parameters OrdIf and ProfileName cannot be used together!" -ForegroundColor Red
        exit
    }
 #>

    if (-not $OrgID) {
        $config = Read-Config
        if ($profileName) {
            $OrgId = $config.profiles.$profileName
            if (-not $OrgID) {
                throw "Invalid profile name!"
            }
        } else {
            $OrgId = $config.profiles.default
        }
    }

    $Headers = Get-Headers

    $Uri = "{0}/organizations/{1}/appliance/vpn/thirdPartyVPNPeers" -f $BaseURI, $OrgID

    $Peers = Get-MerakiOrganizationThirdPartyVPNPeers

    $Peer = @{
        name = $Name
        secret = $Secret
        privateSubnet = $PrivateSubnets
    }
    if ($IkeVersion) { $Peer.Add("ikeVersion", $IkeVersion) }
    if ($IpsecPoliciesPreset) { $Peer.Add("ipsecPoliciesPreset", $IpsecPoliciesPreset) }
    if ($LocalId) { $Peer.Add("localId", $LocalId) }
    if ($PublicIp) { $Peer.Add("publicIp", $PublicIp) }
    if ($RemoteId) { $Peer.Add("networkTags", $NetworkTags) }
    if ($IpsecPolicies) { $Peer.Add("ipsecPolicies", $IpsecPolicies) }

    $Peers += $Peer
    $_Body = @{
        peers = $Peers
    }

    $Body = $_Body | ConvertTo-Json -Depth 5 -Compress

    try {
        $response = Invoke-RestMethod -Method PUT -Uri $Uri -Headers $Headers -Body $Body -PreserveAuthorizationOnRedirect

        return $response
    } catch {
        Throw $_
    }
}

function Get-MerakiOrganizationInventoryDevices() {
    [CmdletBinding(DefaultParameterSetName = 'default')]
    Param(
        [Parameter(ParameterSetName = 'org')]
        [string]$OrgID,
        [Parameter(ParameterSetName = 'profile')]
        [string]$profileName
    )
<#
    If ($OrgId -and $profileName) {
        Write-Host "The parameters OrgId and ProfileName cannot be used together!" -ForegroundColor Red
        return
    }
 #>

    if (-not $OrgID) {
        $config = Read-Config
        if ($profileName) {
            $OrgID = $config.profiles.$profileName
            if (-not $OrgID) {
                throw "Invalid profile name!"
            }
        } else {
            $OrgID = $config.profiles.default
        }        
    }

    $Uri = "{0}/organizations/{1}/inventoryDevices" -f $BaseURI, $OrgID
    $Headers = Get-Headers

    try {
        $response = Invoke-RestMethod -Method GET -Uri $Uri -Headers $Headers -PreserveAuthorizationOnRedirect

        return $response
    } catch {
        throw $_
    }
    <#
    .SYNOPSIS
    Get the organization device inventory
    .PARAMETER OrgID
    Organization ID. If omitted used th default profile.
    .PARAMETER profileName
    Profile name to use. If omitted used th default profile.
    .OUTPUTS
    An array of inventory objects.
    #>

}

Set-Alias -Name GMOrgInvDevices -value Get-MerakiOrganizationInventoryDevices -Option ReadOnly

function Get-MerakiOrganizationSecurityEvents() {
    [CmdLetBinding(DefaultParameterSetName='Default')]
    Param(
        [ValidateScript({$_ -is [datetime]})]
        [Parameter(ParameterSetName = 'dates', Mandatory)]
        [Parameter(ParameterSetName = 'datesWithOrg', Mandatory)]
        [Parameter(ParameterSetName ='datesWithProfiles', Mandatory)]                
        [datetime]$StartDate,

        [ValidateScript({$_ -is [datetime]})]
        [Parameter(ParameterSetName = 'dates', Mandatory)]
        [Parameter(ParameterSetName = 'datesWithOrg', Mandatory)]
        [Parameter(ParameterSetName ='datesWithProfile', Mandatory)]
        [datetime]$EndDate,

        [Parameter(ParameterSetName = 'days', Mandatory)]
        [Parameter(ParameterSetName = 'daysWithOrg', Mandatory)]
        [Parameter(ParameterSetName = 'daysWithProfile', Mandatory)]
        [ValidateScript({$_ -is [int]})]
        [ValidateRange(1,31)]
        [int]$Days,

        [ValidateScript({$_ -is [int]})]
        [ValidateRange(3, 1000)]
        [int]$PerPage,

        [ValidateScript({$_ -is [int]})]
        [int]$Pages,

        [switch]$Descending,

        [Parameter(ParameterSetName = 'org', Mandatory)]
        [Parameter(ParameterSetName = 'datesWithOrg', Mandatory)]
        [Parameter(ParameterSetName = 'daysWithOrg', Mandatory)]
        [string]$OrgId,

        [Parameter(ParameterSetName = 'profile', Mandatory)]
        [Parameter(ParameterSetName = 'datesWithProfile', Mandatory)]
        [Parameter(ParameterSetName = 'daysWithProfile', Mandatory)]
        [string]$ProfileName
    )
    
    $Headers = Get-Headers

    if (-not $OrgID) {
        $config = Read-Config
        if ($profileName) {
            $OrgID = $config.profiles.$profileName
            if (-not $OrgID) {
                throw "Invalid profile name!"
            }
        } else {
            $OrgID = $config.profiles.default
        }        
    }

    $Results = [List[PsObject]]::New()

    $Uri = "{0}/organizations/{1}/appliance/security/events" -f $BaseURI, $OrgId

    Set-Variable -Name Query
    if ($StartDate) {
        $Query = "t0={0}" -f ($StartDate.ToString(")"))
    }
    if ($EndDate) {
        if ($Query) {$Query += "&"}
        $Query = "{0}t1={0}" -f $Query, ($EndDate.ToString("O"))
    }
    if ($Days) {
        $Seconds = [TimeSpan]::FromDays($Days).TotalSeconds
        if ($Query) {$Query += "&"}
        $Query = "{0}timestamp={1}" -f $Seconds
    }
    if ($PerPage) {
        if ($Query) {$Query += "&"}
        $Query = "{0}perPage={1}" -f $Query, $PerPage
    }

    if ($Descending) {
        if ($Query) {$Query += '&'}
        $Query = "{0}sortOrder=descending" -f $Query
    }

    if ($Query) {
        $Uri = "{0}?{1}" -f $Uri, $Query
    }

    try {
        $response = Invoke-WebRequest -Method Get -Uri $Uri -Headers $Headers -PreserveAuthorizationOnRedirect
        [List[PsObject]]$result = $response.Content | ConvertFrom-Json
        if ($result) {
            $Results.AddRange($result)
        }
        $page = 1
        if ($Pages -ne 1) {
            $done = $false
            do {
                if ($response.RelationLink['next']) {
                    $Uri = $response.RelationLink['next']
                    $response = Invoke-WebRequest -Method Get -Uri $Uri -Headers $Headers -PreserveAuthorizationOnRedirect
                    [List[PsObject]]$result = $response.Content | ConvertFrom-Json
                    if ($result) {
                        $Results.AddRange($result)
                    }
                    $page += 1
                    if ($page -gt $Pages) {
                        $done = $true
                    }
                } else {
                    $done = $true
                }
            } until ($done)
        }
        return $response
    } catch {
        throw $_
    }
    <#
    .SYNOPSIS
    Returns security event for the organization.
    .DESCRIPTION
    Returns a collection of security event objects for the given organization. Events can be filtered by dates or timespan.
    .PARAMETER OrgId
    The Organization ID. If omitted and a profile is not specified the default organization is used. Cannot be used with the profile parameter.
    .PARAMETER ProfileName
    Retrieve the Organization Id from the named profile. Cannot be used with the OrgId parameter
    .PARAMETER StartDate
    The starting date to retrieve data. Cannot be more than 365 days prior to today.
    .PARAMETER EndDate
    The ending date to retrieve data. cannot be more than 365 days after StartDate.
    .PARAMETER Days
    The number if days back from today to retrieve data, Cannot be more than 365.
    .PARAMETER PerPage
    Number of entries per page to retrieve. Acceptable range is 3-1000. Default is 100. NOTE: Paging is not implemented.
    .OUTPUTS
    A collection of security event objects.
    #>

}

Set-Alias -Name GMNetSecEvents -Value Get-MerakiOrganizationSecurityEvents

function Get-MerakiOrganizationFirmwareUpgrades() {
    [CmdletBinding(DefaultParameterSetName = 'default')]
    Param(
        [switch]$IncludePending,
        [switch]$includeStarted,
        [switch]$IncludeCompleted,
        [switch]$IncludeCanceled,
        [switch]$IncludeSkipped,
        [switch]$IncludeAppliances,
        [switch]$IncludeCameras,
        [switch]$IncludeCellularGateways,
        [switch]$IncludeSensors,
        [switch]$IncludeSwitches,
        [switch]$IncludeSystemsManagers,
        [switch]$IncludeWireless,
        [ValidateScript({$_ -is [int]})]
        [ValidateSet(3, 1000)]
        [int]$PerPage,
        [Parameter(ParameterSetName = 'org')]
        [string]$OrgId,
        [Parameter(ParameterSetName = 'profile')]
        [string]$ProfileName
    )

    Begin {
        if (-not $OrgID) {
            $config = Read-Config
            if ($profileName) {
                $OrgID = $config.profiles.$profileName
                if (-not $OrgID) {
                    throw "Invalid profile name!"
                }
            } else {
                $OrgID = $config.profiles.default
            }        
        }

        $Headers = Get-Headers

        if ($PerPage) {
            $Query = "perPage={0}" -f $PerPage
        }

        $Statuses = [List[string]]::New()
        if ($IncludePending) {$Statuses.Add("pending")}
        if ($includeStarted) {$Statuses.Add("started")}
        if ($IncludeCanceled) {$Statuses.Add("started")}
        if ($IncludeSkipped) {$Statuses.Add("skipped")}
        if ($IncludeCompleted) {$Statuses.Add("completed")}
        
        if ($Statuses.Count -gt 0) {
            if ($Query) {$Query += "&"}
            $Query = "{0}status[]={1}" -f $Query, ($Statuses.ToArray() -join ',')            
        }

        $ProductTypes = [List[string]]::New()
        if ($IncludeAppliances) {$ProductTypes.Add("appliance")}
        if ($IncludeCameras) {$ProductTypes.Add("camera")}
        if ($IncludeCellularGateways) {$ProductTypes.Add("cellularGateway")}
        if ($IncludeSensors) {$ProductTypes.Add("sensors")}
        if ($IncludeSwitches) {$ProductTypes.Add("switch")}
        if ($IncludeSystemsManagers) {$ProductTypes.Add("systemsManager")}
        if ($IncludeWireless) {$ProductTypes.Add("wireless")}


        if ($ProductTypes.Count -gt 0) {
            if ($Query) {$Query += "&"} else {$Query += "?"}
            $Query = "{0}productTypes[]={1}" -f $Query, ($ProductTypes.ToArray() -join ",")
        }

    }

    Process{

        $Uri = "{0}/organizations/{1}/firmware/upgrades" -f $BaseURI, $OrgId

        if ($Query) {
            $URI = "{0}?{1}" -f $Uri, $Query
        }

        $Results = [List[PsObject]]::New()

        try {
            $response = Invoke-WebRequest -Method GET -Uri $Uri -Headers $Headers -PreserveAuthorizationOnRedirect
            [List[PsObject]]$result = $response.Content | ConvertFrom-Json
            if ($result) {
                $Results.AddRange($result)
            }
            $page = 1
            if ($Pages -ne 1) {
                $done = $false
                do {
                    if ($response.RelationLink['next']) {
                        $Uri = $response.RelationLink['next']
                        $response = Invoke-WebRequest -Method Get -Uri $Uri -Headers $Header -PreserveAuthorizationOnRedirect
                        [List[PsObject]]$result = $response.Content | ConvertFrom-Json
                        if ($result) {
                            $Result.AddRange($result)
                        }
                        $page += 1
                        if ($page -gt $Pages) {
                            $done = $true
                        }
                    } else {
                        $done = $true
                    }
                } until ($done)
            }
            return $Results.ToArray()
        } catch {
            throw $_
        }
    }
        <#
    .SYNOPSIS
    Get firmware upgrade information.
    .DESCRIPTION
    Get firmware upgrade information for an organization
    .PARAMETER OrgId
    The Organization Id. If omitted the retrieved from the default profile.
    .PARAMETER ProfileName
    The profile to retrieve the the Organization ID from.
    .PARAMETER Status
    Filter by this status.
    .PARAMETER ProductType
    Filter by this product type.
    #>

}

Set-Alias -name GMOFirmwareUpgrades -Value Get-MerakiOrganizationFirmwareUpgrades


function Get-MerakiOrganizationFirmwareUpgradesByDevice() {
    [CmdletBinding(DefaultParameterSetName='default')]
    Param(
        [Parameter(ValueFromPipelineByPropertyName)]
        [Alias('NetworkId')]
        [string]$Id,
        [Parameter(ValueFromPipelineByPropertyName)]
        [string]$Serial,
        [string[]]$Macs,
        [string[]]$FirmwareUpgradeIds,
        [string[]]$FirmwareUpgradeBatchIds,
        [ValidateScript({$_ -is [int]})]
        [ValidateRange(3,1000)]
        [int]$PerPage,
        [ValidateScript({$_ -is [int]})]
        [int]$Pages=1,
        [Parameter(ParameterSetName = 'org')]
        [string]$OrgId,
        [Parameter(ParameterSetName = 'profile')]
        [string]$ProfileName
    )
<#
    If ($OrgId -and $profileName) {
        Write-Host "The parameters OrgId and ProfileName cannot be used together!" -ForegroundColor Red
        return
    }
 #>

    Begin {
        $NetworkIds = [List[string]]::New()
        $Serials = [List[string]]::New()

        If (-not $OrgID) {
            $config = Read-Config
            if ($profileName) {
                $OrgId = $config.profiles.$profileName
                if (-not $OrgID) {
                    throw "Invalid profile name!"
                }
            } else {
                $OrgID = $config.profiles.default
            }
        }

        $Headers = Get-Headers

        if ($PerPage) {
            $Query = "perPage={0}" -f $PerPage
        }
        
        if ($Macs) {
            if ($Query) {$Query += '&'}
            $Query = "macs[]={0}" -f ($Macs -join ',')            
        }

        if ($FirmwareUpgradeIds) {
            if ($Query) {$Query += '&'}
            $Query = "{0}firmwareUpgradeIds[]={1}" -f $Query, ($FirmwareUpgradeIds -join ',')            
        }

        if ($FirmwareUpgradeBatchIds) {
            if ($Query) {$Query += '&'}
            $Query = "{0}firmwareUpgradeBatchIds[]={1}" -f $Query, $FirmwareUpgradeBatchIds
        }
        
        $Uri = "{0}/organizations/{1}/firmware/upgrades/byDevice" -f $BaseURI, $OrgId
    }
    
    Process {
        if ($Id) {
            $NetworkIds.Add($Id)
        }
    
        if ($Serial) {
            $Serials.Add($Serial)
        }
    }

    End {

        if ($NetworkIds.Count -gt 0) {
            if ($Query) {$Query += '&'}
            $Query = "{0}networkIds[]={1}" -f $Query, ($NetworkIds.ToArray() -join ',')
        }

        if ($Serials.Count -gt 0) {
            if ($Query) {$Query += "&"}
            $Query = "{0}serials[]={1}" -f $Query, ($Serials.ToArray() -join ',')
        }

        if ($Query) {
            $Uri = "{0}?{1}" -f $Uri, $Query
        }

        $Results = [List[PsObject]]::New()

        Try {
            $response = Invoke-WebRequest -Method GET -Uri $Uri -Headers $Headers -PreserveAuthorizationOnRedirect
            [List[PsObject]]$result = $response.Content | ConvertFrom-Json
            if ($result) {
                $Results.AddRange($result)
            }
            $page = 1
            if ($Pages -ne 1) {
                $done = $false
                do {
                    if ($response.RelationLink['next']) {
                        $Uri = $response.RelationLink['next']
                        $response = Invoke-WebRequest -Method Get -Uri $Uri -Headers $Headers -PreserveAuthorizationOnRedirect
                        [List[PsObject]]$result = $response.Content | ConvertFrom-Json
                        if ($result) {
                            $Results.AddRange($result)
                        }
                        $page += 1
                        if ($page -gt $Pages) {
                            $done = $true
                        }
                    } else {
                        $done = $true
                    }
                } until ($done)
            }

            return $Results.ToArray()
        } catch {
            throw $_
        }
    }
    <#
    .SYNOPSIS
    Get firmware upgrades by Device
    .DESCRIPTION
    Get Meraki organization firmware upgrades by device
    .PARAMETER OrgId
    The organization Id
    .PARAMETER ProfileName
    The saved profile name.
    .PARAMETER PerPage
    Number of entries per page.
    .PARAMETER Id
    An array of network Ids to retrieve upgrades for
    .PARAMETER Serial
    An array of serials to retrieve upgrades for
    .PARAMETER Macs
    An array of MAC Addresses to retrieve upgrades for
    .PARAMETER FirmwareUpgradeIds
    An array of Upgrade Ids to retrieve upgrades for
    .PARAMETER FirmwareUpgradeBatchIds
    An array of Firmware Upgrade Batch Ids to retrieve upgrades for
    .OUTPUTS
    An array of firmware upgrade objects.
    #>

}

#region OrganizationThirdPartyVpnPeers

function Get-MerakiOrganizationDeviceUplinks() {
    [CmdletBinding()]
    Param(
        [Parameter(ValueFromPipelineByPropertyName)]
        [Alias('NetworkId')]
        [string]$Id,
        [Parameter(ValueFromPipelineByPropertyName)]
        [string]$Serial,
        [ValidateScript({$_ -is [int]})]
        [ValidateRange(3,1000)]
        [int]$PerPage,
        [ValidateScript({$_ -is [int]})]
        [int]$Pages,
        [switch]$IncludeAppliances,
        [switch]$IncludeCameras,
        [switch]$IncludeCellularGateways,
        [switch]$IncludeSensors,
        [switch]$IncludeSwitches,
        [switch]$IncludeSystemsManagers,
        [switch]$IncludeWireless,
        [string[]]$Tags,
        [ValidateSet("withAllTags","withAnyTags")]
        [string]$TagFilterType,
        [string]$OrgId,
        [string]$ProfileName
    )

    Begin {
        $Headers = Get-Headers
        $NetworkIds = [List[string]]::New()
        $Serials = [List[string]]::New()

        if (-not $OrgID) {
            $config = Read-Config
            if ($profileName) {
                $OrgID = $config.profiles.$profileName
                if (-not $OrgID) {
                    throw "Invalid profile name!"
                }
            } else {
                $OrgID = $config.profiles.default
            }        
        }
        if ($PerPage) {
            $Query = "?perPage={0}" -f $PerPage
        }

        $ProductTypes = [List[string]]::New()
        if ($IncludeAppliances) {$ProductTypes.Add("appliance")}
        if ($IncludeCameras) {$ProductTypes.Add("camera")}
        if ($IncludeCellularGateways) {$ProductTypes.Add("cellularGateway")}
        if ($IncludeSensors) {$ProductTypes.Add("sensors")}
        if ($IncludeSwitches) {$ProductTypes.Add("switch")}
        if ($IncludeSystemsManagers) {$ProductTypes.Add("systemsManager")}
        if ($IncludeWireless) {$ProductTypes.Add("wireless")}

        if ($ProductTypes.Count -gt 0) {
            if ($Query) {$Query += "&"} else {$Query += "?"}
            $Query = "{0}productTypes[]={1}" -f $Query, ($ProductTypes.ToArray() -join ',')
        }

        if ($Tags) {
            if ($Query) {$Query += "&"} else {$Query += "?"}
            $Query = "{0}tags[]={1}" -f $Query, ($Tags -join ',')
            if ($TagFilterType) {
                $Query = "{0}&tagFilterType={1}" -f $Query, $TagFilterType
            }
        }

        $Uri = "{0}/organizations/{1}/devices/uplinks/addresses/byDevice" -f $BaseURI, $OrgId
    }

    Process {
        if ($id) {
            $NetworkIds.Add($Id)
        }

        if ($Serial) {
            $Serials.Add($Serial)
        }
    }

    End {

        if ($NetworkIds.Count -gt 0) {
            if ($Query) {$Query += "&"} else {$Query += "?"}
            $Query = "{0}networkIds[]={1}" -f $Query, ($NetworkIds.ToArray() -join ',')
        }

        if ($Serials.Count -gt 0) {
            if ($Query) {$Query += "&"} else {$Query += "?"}
            $Query = "{0}serials[]={1}" -f $Query, ($NetworkIds.ToArray() -join ',')
        }

        if ($Query) {
            $Uri = "{0}{1}" -f $Uri, $Query
        }
        $Results = [List[PsObject]]::New()

        try {
            $response = Invoke-WebRequest -Method Get -Uri $Uri -Headers $Headers -PreserveAuthorizationOnRedirect
            [List[PsObject]]$result = $response.Content | ConvertFrom-Json
            if ($result) {
                $Results.AddRange($result)
            }
            $page = 1
            if ($Pages -ne 1) {
                $done = $false
                do {
                    if ($response.RelationLink['next']) {
                        $Uri = $response.RelationLink['next']
                        $response = Invoke-WebRequest -Method Get -Uri $Uri -Headers $Headers
                        [List[PsObject]]$result = $response.Content | ConvertFrom-Json
                        if ($result) {
                            $Results.AddRange($result)
                        }
                        $page += 1
                        if ($page -gt $Pages) {
                            $done = $true
                        }
                    } else {
                        $done = $true
                    }
                } until ($done)
            }

            $Networks = @{}
            Get-MerakiNetworks | ForEach-Object{
                $Networks.Add($_.id, $_.name)
            }
            $Results | ForEach-Object {
                $NetworkName = $Networks[$_.network.id]
                $_.network | Add-Member -MemberType NoteProperty -Name Name -Value $NetworkName 
            }
            return $Results.ToArray()
        } catch {
            throw $_
        }
    }
        <#
        .SYNOPSIS
        List the current uplink addresses for devices in an organization.
        .DESCRIPTION
        List the current uplink addresses for devices in an organization. This can be filtered by
        Networks, Product Types, Serials, and Tags.
        .PARAMETER Id
        Optional parameter to filter device uplinks by network ID.
        .PARAMETER Serial
        Optional parameter to filter device uplinks by device product types.
        .PARAMETER PerPage
        The number of entries per page returned. Acceptable range is 3 - 1000. Default is 1000
        .PARAMETER Pages
        Number of pages to return. Default is 1, 0 = all pages.
        .PARAMETER IncludeAppliances
        Include Appliances
        .PARAMETER IncludeCameras
        Include Cameras
        .PARAMETER IncludeCellularGateways
        Include Cellular Gateways
        .PARAMETER IncludeSensors
        Include Sensors
        .PARAMETER IncludeSwitches
        Include Switches
        .PARAMETER IncludeSystemsManagers
        Include Systems Managers
        .PARAMETER IncludeWireless
        Include Wireless
        .PARAMETER Tags
        An optional parameter to filter devices by tags. The filtering is case-sensitive.
        .PARAMETER TagFilterType
        An optional parameter of value 'withAnyTags' or 'withAllTags' to indicate whether to return devices which contain ANY or ALL of the included tags. If no type is included, 'withAnyTags' will be selected.
        .PARAMETER OrgId
        Organization Id to use.
        .PARAMETER ProfileName
        Named profile to use.
        .OUTPUTS
        An array of device uplink objects.
        .NOTES
        If no include parameters are given then all product typed are returned.
        If one or more include parameters are given then the results are restricted to those product types.
        #>

}

function Get-MerakiOrganizationDeviceStatus() {
    [CmdletBinding()]
    Param(
        [Parameter(ValueFromPipelineByPropertyName)]
        [Alias('NetworkId')]
        [string]$Id,
        [Parameter(ValueFromPipelineByPropertyName)]
        [string]$Serial,
        [Parameter(ParameterSetName = 'org')]
        [string]$OrgId,
        [Parameter(ParameterSetName = 'Profile')]
        [string]$ProfileName,
        [ValidateScript({$_ -is [int]})]
        [ValidateRange(1, 1000)]
        [int]$PerPage,
        [ValidateScript({$_ -is [int]})]
        [int]$Pages = 1,
        [switch]$IncludeAlerting,
        [switch]$includeDormant,
        [switch]$IncludeOffline,
        [switch]$IncludeOnline,
        [switch]$IncludeAppliances,
        [switch]$IncludeCameras,
        [switch]$IncludeCellularGateways,
        [switch]$IncludeSensors,
        [switch]$IncludeSwitches,
        [switch]$IncludeSystemsManagers,
        [switch]$IncludeWireless,
        [string[]]$Models,
        [string[]]$Tags,
        [ValidateSet("withAllTags","withAnyTags")]
        [string]$TagFilterType,
        [datetime]$ConfigUpdatedAfter

    )

    Begin {
        $Headers = Get-Headers
        $NetworkIds = [List[string]]::New()
        $Serials = [List[string]]::New()

        if (-not $OrgID) {
            $config = Read-Config
            if ($profileName) {
                $OrgID = $config.profiles.$profileName
                if (-not $OrgID) {
                    throw "Invalid profile name!"
                }
            } else {
                $OrgID = $config.profiles.default
            }        
        }
        
        $Query = ""
        if ($PerPage) {
            $Query = "perPage={0}" -f $PerPage
        }

        $Statuses = [List[string]]::New()
        if ($IncludeAlerting) { $Statuses.Add("alerting")}
        if ($includeDormant) {$Statuses.Add("dormant")}
        if ($IncludeOffline) {$Statuses.Add("offline")}
        if ($IncludeOnline) {$Statuses.Add("online")}

        if ($Statuses.Count -gt 0) {
            if ($Query) {$Query += "&"}
            $Query = "{0}statuses[]={1}" -f $Query, ($Statuses.ToArray() -join ",")
        }

        $ProductTypes = [List[string]]::New()
        if ($IncludeAppliances) {$ProductTypes.Add("appliance")}
        if ($IncludeCameras) {$ProductTypes.Add("camera")}
        if ($IncludeCellularGateways) {$ProductTypes.Add("cellularGateway")}
        if ($IncludeSensors) {$ProductTypes.Add("sensors")}
        if ($IncludeSwitches) {$ProductTypes.Add("switch")}
        if ($IncludeSystemsManagers) {$ProductTypes.Add("systemsManager")}
        if ($IncludeWireless) {$ProductTypes.Add("wireless")}

        if ($ProductTypes.Count -gt 0) {
            if ($Query) {$Query += "&"}
            $Query = "{0}productTypes[]={1}" -f $Query, ($ProductTypes.ToArray() -join ",")
        }

        if ($Models) {
            if ($Query) {$Query += "&"}
            $Query = "{0}models[]={1}" -f $Query, ($Models -join ",")
        }

        if ($Tags) {
            if ($Query) {$Query += "&"}
            $Query = "{0}tags[]={1}" -f ($Tags -join ",")

            if ($TagFilterType) {
                $Query = "{0}&tagFilterType=" -f $Query, $TagFilterType
            }
        }

        if ($ConfigUpdatedAfter) {
            if ($Query) {$Query += "&"}
            $Query = "{0}configurationUpdatedAfter={1}" -f $Query, ($ConfigUpdatedAfter.ToString("O"))
        }
        
        $Uri = "{0}/organizations/{1}/devices/statuses" -f $BaseUri, $OrgId

    }

    Process {
        if ($id) {            
            $NetworkIds.Add($Id)
        }

        if ($Serial) {
            $Serials.Add($Serial)
        }
    }

    End {

        if ($NetworkIds.Count -gt 0) {
            if ($Query) {$Query += "&"}
            $Query = "{0}networkIds[]={1}" -f $Query, ($NetworkIds.ToArray() -join ",")
        }

        if ($Serials.Count -gt 0) {
            if ($Query) {$Query += "&"}
            $Query = "{0}serials[]={1}" -f $Query, ($Serials.ToArray() -join ",")
        }

        if ($Query) {
            $Uri = "{0}?{1}" -f $Uri, $Query
        }

        $Params = @{
            Method = "Get"
            Uri = $Uri
            Headers = $Headers
        }

        $Results = [List[PsObject]]::New()

        try {
            $response = Invoke-WebRequest @Params -PreserveAuthorizationOnRedirect
            [List[PsObject]]$result = $response.Content | ConvertFrom-Json
            if ($result) {
                $Results.AddRange($result)
            }
            $page = 1
            if ($Pages -ne 1) {
                $done = $false
                do {
                    if ($response.RelationLink['next']) {
                        $Uri = $response.RelationLink['next']
                        $response = Invoke-WebRequest -Method Get -Uri $Uri -Headers $Headers
                        [List[PsObject]]$result = $response.Content | ConvertFrom-Json
                        if ($result) {
                            $Results.AddRange($result)
                        }
                        $page += 1
                        if ($page -gt $Pages) {
                            $done = $true
                        }
                    } else {
                        $done = $true
                    }
                } until ($done)
            }
            $Results | ForEach-Object {
                $NetworkName = (Get-MerakiNetwork -networkID $_.networkId).name
                $_ | Add-Member -MemberType NoteProperty -Name "NetworkName" -Value $NetworkName
            }
            return $Results.ToArray()
        } catch {
            throw $_
        }
    }
    <#
    .SYNOPSIS
    List the status of every Meraki device in the organization.
    .DESCRIPTION
    List the status of every Meraki device in the organization. Can be filtered by Network or Serial number.
    .PARAMETER Id
    The ID of the Network to retrieve device status from.
    .PARAMETER Serial
    Then serial number of the device to retrieve status from.
    .PARAMETER OrgId
    The organization Id to use.
    .PARAMETER ProfileName
    The named profile to use.
    .OUTPUTS
    An array of device status objects
    #>

}

function Get-MerakiOrganizationApplianceVpnStatuses() {
    [CmdletBinding()]
    Param(
        [Parameter(ValueFromPipelineByPropertyName)]
        [Alias('NetworkId')]
        [string]$id,
        [ValidateScript({$_ -is [int]})]
        [ValidateSet(3,300)]
        [int]$PerPage,
        [ValidateScript({$_ -is [int]})]
        [int]$Pages = 1,
        [Parameter(ParameterSetName = 'org')]
        [string]$OrgId,
        [Parameter(ParameterSetName = 'profile')]
        [string]$ProfileName
    )

    Begin {
        $Headers = Get-Headers

        if (-not $OrgID) {
            $config = Read-Config
            if ($profileName) {
                $OrgID = $config.profiles.$profileName
                if (-not $OrgID) {
                    throw "Invalid profile name!"
                }
            } else {
                $OrgID = $config.profiles.default
            }        
        }

        $NetworkIds = [List[String]]::New()

        if ($PerPage) {
            $Query = "?perPage={0}" -f $PerPage
        }

        $Uri = "{0}/organizations/{1}/appliance/vpn/statuses" -f $BaseURI, $OrgId
    }

    Process {
        if ($id) {
            $NetworkIds.Add($Id)
        }
    }

    End {
        $Results = [List[PsObject]]::New()

        if ($NetworkIds.Count -gt 0) {
            if ($Query) {$Query += "&"} else {$Query += "?"}
            $Query = "{0}networkIds[]={1}" -f $Query, ($NetworkIds.ToArray() -join ',')
        }

        try {
            $response = Invoke-WebRequest -Method Get -Uri $Uri -Headers $Headers -PreserveAuthorizationOnRedirect
            [List[PsObject]]$result = $response.Content | ConvertFrom-Json
            if ($result) {
                $Results.AddRange($result)
            }
            $page = 1
            if ($Pages -ne 1) {
                $done = $false
                do {
                    if ($response.RelationLink['next']) {
                        $Uri = $response.RelationLink['next']
                        $response = Invoke-WebRequest -Method Get -Uri $Uri -Headers $Headers -PreserveAuthorizationOnRedirect
                        [List[PsObject]]$result = $response.Content | ConvertFrom-Json
                        if ($result) {
                            $Results.AddRange($result)
                        }
                        $page += 1
                        if ($page =gt $Pages) {
                            $done = $true
                        }
                    } else {
                        $done = $true
                    }
                 } until ($done)
            }
            return $Results.ToArray()
        } catch {
            throw $_
        }
    }
    <#
    .SYNOPSIS
    List VPN status for networks in an organization
    .DESCRIPTION
    Show VPN status for networks in an organization. can be filtered by Networks.
    .PARAMETER id
    The network ID to get VPN status for.
    .PARAMETER PerPage
    The number of entries per page returned. Acceptable range is 3 - 300. Default is 300.
    .PARAMETER Pages
    The number of pages to return. Default is 1, 0 = return all pages.
    .PARAMETER OrgId
    The organization to use.
    .PARAMETER ProfileName
    The named profile to use.
    .OUTPUTS
    An array of VPN statuses.
    #>

}