unifiPS.psm1

$Script:WebSession = $null
$Script:BaseUri = $null
$Script:RestHeaders = $null

#region internal functions
function Invoke-UnifiRestCall {
    [CmdletBinding()]
    [OutputType([Object])]

    param(
        # HTTP Method
        [Parameter(Mandatory = $True)]
        [ValidateSet("GET","POST","PUT","DELETE")]
        [string]
        $Method,

        # REST route (URI)
        [Parameter(Mandatory = $True)]
        [string]
        $Route,

        # Body for Invoke-RestMethod (will only be applied if $Method is POST, PUT or DELETE)
        [Parameter(Mandatory = $False)]
        [Object]
        $Body,

        # Custom Parameters for Invoke-RestMethod
        [Parameter(Mandatory = $False)]
        [Object]
        $CustomRestParams
    )

    process {
        $restParams = @{
            Headers = @{"charset"="utf-8";"Content-Type"="application/json"}
            TimeoutSec = $script:Timeout
            Uri = $($script:BaseUri) + "/" + $Route
            WebSession = $Script:WebSession
            Method = $Method
            Verbose = $false
        }

        if ($script:useSkipCertParam) {
            $restParams.SkipCertificateCheck = $true
        }

        if ($CustomRestParams) {
            $restParams = $CustomRestParams
        }

        if (@("POST","PUT","DELETE") -contains $Method) {
            $restParams.Body = $Body
        }

        Write-Verbose "Calling $($restParams.Uri) [$($restParams.Method)]"
        try {
            $json = Invoke-RestMethod @restParams
        } catch [System.Net.WebException] {
            $json = $_.ErrorDetails | ConvertFrom-Json
            $ErrorCode = $json.meta.msg
            Write-Error "Error while accessing rest endpoint '$Route' ($Method): $ErrorCode"
        } catch {
            Write-Error "Other error while accessing rest endpoint '$Route' ($Method): $_"
        } finally {
            if ($json) {
                $json
            }

            if ($restParams.SessionVariable) {
                $script:WebSession = $WebSession
            }
        }
    }
}
#endregion

#region Authentication
function Invoke-UnifiLogin {
    <#
    .SYNOPSIS
        Makes a RestMethod request to the unifi api, which will hopefully login the given user
    .DESCRIPTION
        Makes a RestMethod request to the unifi api, which will hopefully login the given user
        Credentials can be directly used with $Credentials-Parameter (you will be asked for credentials if this parameter is omitted).
        If the login succeeds a WebSession is saved to $Script:WebSession
 
        A timeout can be specified for the webrequest
    .EXAMPLE
        PS C:\> Invoke-UnifiLogin -Uri https://localhost:8443/api -Timeout 5
        Logs in to the unifi server at the specified address and wait max. 5 seconds
    .OUTPUTS
        Returns $True on Success
        Returns $False on Failure
    #>

    [CmdletBinding()]
    [OutputType([Boolean])]

    param(
        # Uri of the UniFi Server
        [Parameter(
            Mandatory = $true
        )]
        [string]
        $Uri,

        # Login credentials
        [Parameter(
            Mandatory = $false
        )]
        [System.Management.Automation.PSCredential]
        $Credential,

        # Timeout in seconds
        [Parameter(
            Mandatory = $false
        )]
        [ValidateNotNullOrEmpty()]
        [Int]
        $Timeout= 5
    )

    process {
        $script:BaseUri = $Uri
        $script:Timeout = $Timeout
        $Script:WebSession = $null

        $TestSkipCertParam = (Get-Command Invoke-RestMethod).Parameters.SkipCertificateCheck
        if ($TestSkipCertParam) { # Parameter to skip cert is available, so why not use it
            $script:useSkipCertParam = $true
        } else { # Parameter to skip cert is not available, try a workaround
            try {
                add-type @"
using System.Net;
using System.Security.Cryptography.X509Certificates;
public class TrustAllCertsPolicy : ICertificatePolicy {
    public bool CheckValidationResult(
        ServicePoint srvPoint, X509Certificate certificate,
        WebRequest request, int certificateProblem) {
        return true;
    }
}
"@

                [System.Net.ServicePointManager]::CertificatePolicy = New-Object TrustAllCertsPolicy
            } catch {}
        }

        if (!($Credential)) {
            $Credential = (Get-Credential -Message "Login for UniFi-Controller $($script:BaseUri)")
        }
        $Body = @{ "username" = $Credential.UserName; "password" = $Credential.GetNetworkCredential().Password } | ConvertTo-JSON

        $restParams = @{
            Headers = @{"charset"="utf-8";"Content-Type"="application/json"}
            TimeoutSec = $script:Timeout
            Uri = $($script:BaseUri) + "/api/login"
            SessionVariable = "WebSession"
            Verbose = $false
            Method = "Post"
        }

        if ($script:useSkipCertParam) {
            $restParams.SkipCertificateCheck = $true
        }

        $jsonResult = Invoke-UnifiRestCall -Method POST -Route "login" -Body $Body -CustomRestParams $restParams

        $Credential = $null
        $Body = $null

        if ($jsonResult.meta.rc -eq "ok") {
            Write-Verbose "Login to Unifi-Controller successful"
            return $True
        } else {
            Write-Error "Login to Unifi-Controller failed"
            return $False
        }
    }
}

function Invoke-UnifiLogout {
    <#
    .SYNOPSIS
        Logs out of the unifi server and destroys the websession
    .DESCRIPTION
        Logs out of the unifi server and destroys the websession
    .EXAMPLE
        PS C:\> Invoke-UnifiLogout
        Logs out of the server
    .OUTPUTS
        Returns $True on Success
        Returns $False on Failure
    #>

    [CmdletBinding()]
    [OutputType([Boolean])]

    param()

    process {
        $jsonResult = Invoke-UnifiRestCall -Method POST -Route "api/logout"

        if ($jsonResult.meta.rc -eq "ok") {
            Write-Verbose "Logout from Unifi-Controller successful"
            return $True
        } else {
            Write-Error "Logout from Unifi-Controller failed"
            return $False
        }
        
    }
}
#endregion

#region Unifi Controller information
function Get-UnifiServerInfo {
    <#
    .SYNOPSIS
        Grabs simple information from the unifi server (state,version,uuid)
    .DESCRIPTION
        Grabs simple information from the unifi server (state,version,uuid)
        You do not need to be logged in to grap this information
    .EXAMPLE
        PS C:\> Get-UnifiServerInfo
        Grabs the information
    .OUTPUTS
        Returns JSON-Data
    #>

    [CmdletBinding()]
    [OutputType([Object])]

    param(
        # Do not filter or rename output, just sent the json result back as raw data
        [Parameter(Mandatory = $false)]
        [switch]
        $Raw
    )
   
    process {
        $jsonResult = Invoke-UnifiRestCall -Method GET -Route "status"

        if ($jsonResult.meta.rc -eq "ok") {
            if ($Raw) {
                $jsonResult.meta
            } else {
                $jsonResult.meta | Select-Object UUID,@{N="Version";E={$_.server_version}},@{N="URI";E={$Script:BaseUri}}
            }
        }
    }
}
#endregion

#region User information
function Get-UnifiLogin {
    <#
    .SYNOPSIS
        Shows information about the currently logged in user
    .DESCRIPTION
        Shows information about the currently logged in user
    .EXAMPLE
        PS C:\> Get-UnifiLogin
        Shows information about the currently logged in user
    .OUTPUTS
        Returns JSON-Data
    #>

    [CmdletBinding()]
    [OutputType([Object])]

    param(
        # Do not filter or rename output, just sent the json result back as raw data
        [Parameter(Mandatory = $false)]
        [switch]
        $Raw
    )

    process {
        $jsonResult = Invoke-UnifiRestCall -Method GET -Route "api/self"

        if ($jsonResult.meta.rc -eq "ok") {
            if ($Raw) {
                $jsonResult.data
            } else {
                $jsonResult.data | Select-Object Name,@{N="AdminID";E={$_.admin_id}},EMail,@{N="EMailAlert";E={$_.email_alert_enabled}},@{N="SuperAdmin";E={$_.is_super}},@{N="UISettings";E={$_.ui_settings}}
            }
        }
    }
}

function Get-UnifiAdmin {
    <#
    .SYNOPSIS
        Lists unifi admins for all or just one site
    .DESCRIPTION
        Lists unifi admins for all or just one site
    .EXAMPLE
        PS C:\> Get-UnifiAdmin -All
        Lists unifi admins for all sites
         
        PS C:\> Get-UnifiAdmin -SiteName "Default"
        Lists unifi admins for site "Default"
    .OUTPUTS
        Returns JSON-Data
    #>

    [CmdletBinding()]
    [OutputType([Object])]

    param(
        # SiteName
        [Parameter(Mandatory = $false, ParameterSetName="SiteName", ValueFromPipelineByPropertyName=$True)]
        [string]
        $SiteName,

        # Do not filter or rename output, just sent the json result back as raw data
        [Parameter(Mandatory = $false)]
        [switch]
        $Raw,

        # List Admins for all sites
        [Parameter(Mandatory = $false, ParameterSetName="All")]
        [switch]
        $All
    )
   
    process {
        if (!$All -and [string]::IsNullOrWhiteSpace($SiteName)) {
            Write-Error "No SiteName was given"
        } else {
            if ($All) {
                $jsonResult = Invoke-UnifiRestCall -Method GET -Route "api/stat/admin"
            } else {
                $jsonResult = Invoke-UnifiRestCall -Method POST -Route "api/s/$($SiteName)/cmd/sitemgr" -Body (@{cmd = "get-admins"} | ConvertTo-JSON)
            }

            if ($jsonResult.meta.rc -eq "ok") {
                if ($Raw) {
                    $jsonResult.data
                } else {
                    if ($All) {
                        $jsonResult.data | Select-Object    name,email,
                                                        @{N="UserID";E={$_._id}},
                                                        @{N="SuperAdmin";E={$_.is_super}},
                                                        @{N="Roles";E={$_.roles}},
                                                        @{N="SuperRoles";E={$_.super_roles}},
                                                        @{N="CreatedOn";E={ ( Get-Date('1970-01-01 00:00:00') ).AddSeconds($_.time_created) }},
                                                        @{N="LastSiteName";E={$_.last_site_name}},
                                                        @{N="EMailAlert";E={$_.email_alert_enabled}}
                    } else {
                        $jsonResult.data | Select-Object    name,email,
                                                        @{N="UserID";E={$_._id}},
                                                        @{N="Permissions";E={$_.permissions}},
                                                        @{N="SuperAdmin";E={$_.is_super}},
                                                        @{N="Role";E={$_.role}},
                                                        @{N="EMailAlert";E={$_.email_alert_enabled}}
                    }
                }
            }
        }
    }
}
#endregion

#region Basic site handling
function Get-UnifiSite {
    <#
    .SYNOPSIS
        Gets one or more sites of the unifi controller
    .DESCRIPTION
        Gets one or more sites of the unifi controller
        You can filter by SiteName (internal site name) or SiteID or SiteDisplayName (name visible in the web interface, unifi's internal name for this field is 'desc')
    .EXAMPLE
        PS C:\> Get-UnifiSite -DisplayName *
        Lists all sites
    .EXAMPLE
        PS C:\> Get-UnifiSite -DisplayName "Default","*Test*"
        Lists all sites which contains the string "Test" and the site with the name "Default"
    .EXAMPLE
        PS C:\> Get-UnifiSite -SiteName "67itznop"
        Lists the site with the SiteName '67itznop'
    .EXAMPLE
        PS C:\> Get-UnifiSite SiteID "623e1bf66a5d4f1280160b7e"
        Lists the site with the ID '623e1bf66a5d4f1280160b7e'
    .OUTPUTS
        Returns JSON-Data
    #>

    [CmdletBinding(DefaultParameterSetName="SiteDisplayName")]
    [OutputType([Object])]

    param(
        # Name of the site (Unifi's internal name is used, not the name visible in the web interface)
        [Parameter( ParameterSetName = "SiteName", Mandatory = $true, ValueFromPipelineByPropertyName = $true )]
        [String[]]
        $SiteName,

        # ID of the site
        [Parameter( ParameterSetName = "SiteID", Mandatory = $true, ValueFromPipelineByPropertyName = $true )]
        [String[]]
        $SiteID,

        # friendlyName of the site (Unifi's internal name for this field is 'desc'). This is the value visible in the web interface
        [Parameter( ParameterSetName = "SiteDisplayName", Mandatory = $true, ValueFromPipelineByPropertyName = $true, Position = 0 )]
        [Alias("SiteDescription")]
        [String[]]
        $SiteDisplayName,

        # Do not filter or rename output, just sent the json result back as raw data
        [Parameter(Mandatory = $false)]
        [switch]
        $Raw
    )
   
    process {
        try {
            $jsonResult = Invoke-UnifiRestCall -Method GET -Route "api/self/sites"

            if ($jsonResult.meta.rc -eq "ok") {

                switch ($PSCmdlet.ParameterSetName) {
                    "SiteName" { 
                        $tmpList = @()
                        foreach($singleSiteName in $SiteName) {
                            $tmpList += $jsonResult.data | Where-Object { $_.Name -like $singleSiteName }
                        }
                        $jsonResult.data = $tmpList
                    }
                    "SiteID" {
                        $tmpList = @()
                        foreach($singleSiteID in $SiteID) {
                            $tmpList += $jsonResult.data | Where-Object { $_._id -like $singleSiteID }
                        }
                        $jsonResult.data = $tmpList
                    }
                    "SiteDisplayName" { 
                        $tmpList = @()
                        foreach($singleSiteDisplayName in $SiteDisplayName) {
                            $tmpList += $jsonResult.data | Where-Object { $_.desc -like $singleSiteDisplayName }
                        }
                        $jsonResult.data = $tmpList
                    }
                }

                if ($Raw) {
                    $jsonResult.data 
                } else {
                    $jsonResult.data | Select-Object    @{N="SiteID";E={$_._id}},
                                                        @{N="SiteDisplayName";E={$_.desc}},
                                                        @{N="SiteName";E={$_.name}},
                                                        @{N="NoDelete";E={ if ($_.attr_no_delete) {$_.attr_no_delete} else { $False }}}
                }
            }

        } catch {
            Write-Error "Something went wrong while fetching sites ($($_.Exception))" -ErrorAction Stop
        }
    }
}

function New-UnifiSite {
    <#
    .SYNOPSIS
        Creates a new unifi site
    .DESCRIPTION
        Creates a new unifi site.
        It does check if a site with the same name is already present (You can have more than one site with the same DisplayName in the unifi controller (a bit stupid if you ask me...))
        If you want to disable this check, use the 'DisableNameCheck'-Switch
    .EXAMPLE
        PS C:\> New-UnifiSite -SiteDisplayName "My New Site"
        Creates the new unifi site 'My New Site'
    .EXAMPLE
        PS C:\> New-UnifiSite -SiteDisplayName "My New Site" DisableNameCheck
        Creates the new unifi site 'My New Site' even if a site with this DisplayName is already present
    .OUTPUTS
        Returns JSON-Data from the newly created site
    #>

    [CmdletBinding()]
    [OutputType([Object])]

    param(
        # (Display-)Name of the site under which it appears in the webui
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ValueFromPipeline = $true, Position = 0 )]
        [String]
        $SiteDisplayName,

        # Disable checking if a site name is already present
        [Parameter(Mandatory = $false)]
        [switch]
        $DisableNameCheck,

        # Do not filter or rename output, just sent the json result back as raw data
        [Parameter(Mandatory = $false)]
        [switch]
        $Raw
    )
   
    process {
        try {

            if (!$DisableNameCheck) {
                $sites = Get-UnifiSite "*"

                if ($sites.SiteDisplayName -contains $SiteDisplayName) {
                    Write-Error "There's already a site with the DisplayName '$SiteDisplayName' present."
                    return ""
                }
            }

            $Body = @{
                cmd = "add-site"
                desc = $SiteDisplayName
            } | ConvertTo-Json

            $jsonResult = Invoke-UnifiRestCall -Method POST -Route "api/s/default/cmd/sitemgr" -Body $Body

            if ($jsonResult.meta.rc -eq "ok") {
                Write-Verbose "Site '$SiteDisplayName' successfully created"

                if ($Raw) {
                    $jsonResult.data
                } else {
                    $jsonResult.data | Select-Object    @{N="SiteName";E={$_.name}},
                                                        @{N="SiteID";E={$_._id}},
                                                        @{N="SiteDisplayName";E={$_.desc}}

                }
            } else {
               Write-Error "Site '$SiteDisplayName' was NOT created ($jsonResult.meta.msg)" 
            }

        } catch {
            Write-Warning "Something went wrong while creating a new site $($SiteDisplayName) ($_)"
        }
    }
}

function Remove-UnifiSite {
    <#
    .SYNOPSIS
        Deletes a unifi site
    .DESCRIPTION
        Deletes a unifi site. Be careful with this!
    .EXAMPLE
        PS C:\> Remove-UnifiSite -SiteName 67itznop
        Removes the unifi site with the SiteName '67itznop', but asks for confirmation
    .EXAMPLE
        PS C:\> Get-UnifiSite -SiteDisplayName 'ProductionSite' | Remove-UnifiSite -Force
        Removes the unifi site with the DisplayName 'ProductionSite' and does NOT ask for confirmation
    .OUTPUTS
        Returns $True on Success
        Returns $False on Failure
    #>

    [CmdletBinding()]
    [OutputType([boolean])]

    param(
        # Name of the site (Unifi's internal name is used, not the name visible in the web interface)
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true )]
        [String]
        $SiteName,

        # Do not ask for confirmation
        [Parameter(Mandatory = $false)]
        [switch]
        $Force
    )
   
    process {
        try {
            $site = Get-UnifiSite -SiteName $SiteName

            if ($site) {
                if (!$Force) {
                    do {
                        $answer = Read-Host -Prompt "Do you really want to delete the site '$($SiteName)' (DisplayName: $($site.SiteDisplayName))? Be **extremely careful with this** (y/N): "
                    } while($answer -ne "y" -and $answer -ne "n" -and $answer -ne "")

                    if ($answer -eq "" -or $answer -eq "n") {
                        Write-Verbose "Deletion of site '$($SiteName)' (DisplayName: $($site.SiteDisplayName)) was aborted by user"
                        return $False
                    }

                }

                $Body = @{
                    site = $site.SiteID
                    cmd = "delete-site"
                } | ConvertTo-Json
                $jsonResult = Invoke-UnifiRestCall -Method POST -Route "api/s/$($SiteName)/cmd/sitemgr" -Body $Body

                if ($jsonResult.meta.rc -eq "ok") {
                    Write-Verbose "Site '$($SiteName)' (DisplayName: $($site.SiteDisplayName)) successfully deleted"
                    return $True
                } else {
                    Write-Error "Site '$($SiteName)' (DisplayName: $($site.SiteDisplayName)) was NOT deleted"
                    return $False
                }
            } else {
                Write-Error "No site '$SiteName' was found"
                return $False
            }

        } catch {
            Write-Warning "Something went wrong while removing site $($SiteName) ($_)"
            return $False
        }
    }
}

function Rename-UnifiSite {
    <#
    .SYNOPSIS
        Renames a unifi site
    .DESCRIPTION
        Renames a unifi site
    .EXAMPLE
        PS C:\> Rename-UnifiSite -SiteName '67itznop' -NewSiteDisplayName "my wonderful site"
        Renames the unifi site with the SiteName '67itznop' to 'my wonderful site'. Note that the SiteName keeps the same. Only the SiteDisplayName in the webui changes
    .EXAMPLE
        PS C:\> Get-UnifiSite -SiteDisplayName 'Development' | Rename-UnifiSite
        Renames the unifi site with the SiteName '67itznop' to 'my wonderful site'. Note that the SiteName keeps the same. Only the SiteDisplayName in the webui changes
    .OUTPUTS
        Returns JSON-Data
    #>

    [CmdletBinding()]
    [OutputType([Object])]

    param(
        # Name of the site which will be renamed (Unifi's internal name is used, not the name visible in the web interface)
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true )]
        [String]
        $SiteName,

        # New DisplayName of the site
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true )]
        [Alias("SiteDisplayName")]
        [String]
        $NewSiteDisplayName,

        # Do not filter or rename output, just sent the json result back as raw data
        [Parameter(Mandatory = $false)]
        [switch]
        $Raw
    )
   
    process {
        try {
            $site = Get-UnifiSite -SiteName $SiteName

            if ($site) {

                if ($site.SiteDisplayName -eq $NewSiteDisplayName) {
                    Write-Warning "Nothing to do. Old and new display names match"
                } else {

                    $Body = @{
                        desc = $NewSiteDisplayName
                        cmd = "update-site"
                    } | ConvertTo-Json

                    $jsonResult = Invoke-UnifiRestCall -Method POST -Route "api/s/$($SiteName)/cmd/sitemgr" -Body $Body

                    if ($jsonResult.meta.rc -eq "ok") {
                        Write-Verbose "Site '$($SiteName)' was renamed from '$($site.NewSiteDisplayName)' to '$NewSiteDisplayName'"
                        if ($Raw) {
                            $jsonResult.data
                        } else {
                            $jsonResult.data | Select-Object    @{N="SiteID";E={$_._id}},
                                                                @{N="NewSiteDisplayName";E={$_.desc}},
                                                                @{N="SiteName";E={$_.name}},
                                                                @{N="NoDelete";E={ if ($_.attr_no_delete) {$_.attr_no_delete} else { $False }}}
                        }

                    } else {
                        Write-Error "Site '$($SiteName)' (DisplayName: $($site.NewSiteDisplayName)) was NOT renamed"
                    }
                }
            } else {
                Write-Error "No site '$SiteName' was found"
            }

        } catch {
            Write-Warning "Something went wrong while renaming site $($SiteName) ($_)"
        }
    }
}

function Get-UnifiSiteInfo {
    <#
    .SYNOPSIS
        Gets extended information for a Unifi site
    .DESCRIPTION
        Gets extended information for a Unifi site like status for
        wlan (status, # APs, # adopted, # disabled, # disconnected, # pending, # users, # guests)
        wan (status, # adopted, # pending, # gateways)
        www (status, )
        lan (status, # adopted, #disconnected, # pending, # sw(?))
        vpn (status).
 
        You can pipe the output from "Get-UnifiSite" to this cmdlet
    .EXAMPLE
        PS C:\> Get-UnifiSiteInfo -SiteName default
        Gets information from the default site
    .EXAMPLE
        PS C:\> Get-UnifiSite * | Get-UnifiSiteInfo
        Gets information from all sites
    .OUTPUTS
        Returns JSON-Data
    #>

    [CmdletBinding()]
    [OutputType([Object])]

    param(
        # Name of the site (Unifi's internal name is used, not the name visible in the web interface)
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true )]
        [String]
        $SiteName,

        # Do not filter or rename output, just sent the json result back as raw data
        [Parameter(Mandatory = $false)]
        [switch]
        $Raw
    )
   
    process {
        
        try {
            $jsonResult = Invoke-UnifiRestCall -Method GET -Route "api/s/$($SiteName)/stat/health"

            if ($jsonResult.meta.rc -eq "ok") {

                if ($Raw) {
                    $jsonResult.data 
                } else {
                    foreach ($subsystem in $jsonResult.data) {
                        switch ($subsystem.subsystem) {
                            "wlan" {
                                $subsystem | Select-Object  @{N="SiteName";E={$SiteName}},
                                                            Subsystem, Status,
                                                            @{N="APs";E={$_.num_ap}},
                                                            @{N="Adopted";E={$_.num_adopted}},
                                                            @{N="Disabled";E={$_.num_disabled}},
                                                            @{N="Disconnected";E={$_.num_disconnected}},
                                                            @{N="Pending";E={$_.num_pending}},
                                                            @{N="Users";E={$_.num_user}},
                                                            @{N="Guests";E={$_.num_guest}},
                                                            @{N="IOT";E={$_.num_iot}},
                                                            @{N="TX";E={$_."tx_bytes-r"}},
                                                            @{N="RX";E={$_."rx_bytes-r"}}
                            }

                            "wan" {
                                $subsystem | Select-Object  @{N="SiteName";E={$SiteName}},
                                                            Subsystem, Status,
                                                            @{N="Gateways";E={$_.num_gw}},
                                                            @{N="Adopted";E={$_.num_adopted}},
                                                            @{N="Disconnected";E={$_.num_disconnected}},
                                                            @{N="Pending";E={$_.num_pending}},
                                                            @{N="IP";E={$_.wan_ip}},
                                                            @{N="Gateway";E={$_.gateways}},
                                                            @{N="Netmask";E={$_.netmask}},
                                                            @{N="Nameservers";E={$_.nameservers}},
                                                            @{N="MAC";E={$_.gw_mac}},
                                                            @{N="Name";E={$_.gw_name}},
                                                            @{N="Version";E={$_.gw_version}},
                                                            @{N="Uptime";E={$_.uptime_stats}},
                                                            @{N="Stats";E={$_.'gw_system-stats'}},
                                                            @{N="TX";E={$_."tx_bytes-r"}},
                                                            @{N="RX";E={$_."rx_bytes-r"}},
                                                            @{N="STA";E={$_."num_sta"}}
                            }

                            "www" {
                                $subsystem | Select-Object  @{N="SiteName";E={$SiteName}},
                                                            Subsystem, Status,
                                                            @{N="TX";E={$_."tx_bytes-r"}},
                                                            @{N="RX";E={$_."rx_bytes-r"}},
                                                            @{N="Latency";E={$_.latency}},
                                                            @{N="Uptime";E={$_.Uptime}},
                                                            @{N="Drops";E={$_.Drops}},
                                                            @{N="Up";E={$_.xput_up}},
                                                            @{N="Down";E={$_.xput_down}},
                                                            @{N="SpeedtestStatus";E={$_.speedtest_status}},
                                                            @{N="SpeedtestLastRun";E={$_.speedtest_lastrun}},
                                                            @{N="SpeedtestPing";E={$_.speedtest_ping}},
                                                            @{N="MAC";E={$_.gw_mac}}
                            }

                            "lan" {
                                $subsystem | Select-Object  @{N="SiteName";E={$SiteName}},
                                                            Subsystem, Status,
                                                            @{N="Users";E={$_.num_user}},
                                                            @{N="Guests";E={$_.num_guest}},
                                                            @{N="IOT";E={$_.num_iot}},
                                                            @{N="TX";E={$_."tx_bytes-r"}},
                                                            @{N="RX";E={$_."rx_bytes-r"}},
                                                            @{N="Switche";E={$_.num_sw}},
                                                            @{N="Adopted";E={$_.num_adopted}},
                                                            @{N="Disconnected";E={$_.num_disconnected}},
                                                            @{N="Pending";E={$_.num_pending}}
                            }

                            "vpn" {
                                $subsystem | Select-Object  @{N="SiteName";E={$SiteName}},
                                                            Subsystem, Status
                            }
                        }
                    }
                    
                }
            }

        } catch {
            Write-Error "Something went wrong while fetching sites ($($_.Exception))" -ErrorAction Stop
        }
        
    }
}
#endregion

#region Log/Events/Alarms
function Get-UnifiEvent {
    <#
    .SYNOPSIS
        Gets events for a unifi site
    .DESCRIPTION
        Gets events for a unifi site. The default limit for events is 500. Use a limit of 0 to disable this limit. But note that the unifi controller api has a max limit of 3000 entries
 
        You can pipe the output from "Get-UnifiSite" to this cmdlet
    .EXAMPLE
        PS C:\> Get-UnifiSite -DisplayName "Test" | Get-UnifiEvent
        Gets events from site with the DisplayName "Test"
    .EXAMPLE
        PS C:\> Get-UnifiEvent -SiteName "01gg6pt0" -Limit 0
        Gets events from the site with the (internal) name "01gg6pt0". Use the unifi controllers default limit of 3000 entries
    .OUTPUTS
        Returns JSON-Data
    #>

    [CmdletBinding()]
    [OutputType([Object])]

    param(
        # Name of the site (Unifi's internal name is used, not the name visible in the web interface)
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true )]
        [String]
        $SiteName,

        # Do not filter or rename output, just sent the json result back as raw data
        [Parameter(Mandatory = $false)]
        [switch]
        $Raw,

        # Limit the number of results as the output can be too big and slow. Zero means no limit
        [Parameter(Mandatory = $false)]
        [int16]
        $Limit = 500
    )
   
    process {
        
        try {
            $jsonResult = Invoke-UnifiRestCall -Method GET -Route "api/s/$($SiteName)/stat/event"

            if ($jsonResult.meta.rc -eq "ok") {

                if ($Limit -gt 0) {
                    $jsonResult.data = $jsonResult.data | Select-Object -First $Limit
                }
                if ($Raw) {
                    $jsonResult.data
                } else {
                    $jsonResult.data | Select-Object -ExcludeProperty "site_id","key","msg","_id","time","is_negative" @{N="SiteName";E={$SiteName}},
                                                        @{N="SiteID";E={$_.site_id}},
                                                        @{N="Category";E={$_.subsystem}},
                                                        @{N="Date";E={$_.DateTime}},
                                                        @{N="EventType";E={$_.key}},
                                                        @{N="Message";E={$_.msg}},
                                                        *
                    
                }
            }

        } catch {
            Write-Error "Something went wrong while fetching sites ($($_.Exception))" -ErrorAction Stop
        }
        
    }
}

function Get-UnifiAlarm {
    <#
    .SYNOPSIS
        Gets alarms for a unifi site
    .DESCRIPTION
        Gets alarms for a unifi site. The default limit for alarms is 500. Use a limit of 0 to disable this limit. But note that the unifi controller api has a max limit of 3000 entries
 
        You can pipe the output from "Get-UnifiSite" to this cmdlet
    .EXAMPLE
        PS C:\> Get-UnifiSite -DisplayName "Test" | Get-UnifiAlarm
        Gets alarms from site with the DisplayName "Test"
    .EXAMPLE
        PS C:\> Get-UnifiAlarm -SiteName "01gg6pt0"
        Gets alarms from the site with the (internal) name "01gg6pt0"
    .OUTPUTS
        Returns JSON-Data
    #>

    [CmdletBinding()]
    [OutputType([Object])]

    param(
        # Name of the site (Unifi's internal name is used, not the name visible in the web interface)
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true )]
        [String]
        $SiteName,

        # Do not filter or rename output, just sent the json result back as raw data
        [Parameter(Mandatory = $false)]
        [switch]
        $Raw,

        # Limit the number of results as the output can be too big and slow. Zero means no limit
        [Parameter(Mandatory = $false)]
        [int16]
        $Limit = 500
    )
   
    process {
        
        try {
            $jsonResult = Invoke-UnifiRestCall -Method GET -Route "api/s/$($SiteName)/stat/alarm"

            if ($jsonResult.meta.rc -eq "ok") {

                if ($Limit -gt 0) {
                    $jsonResult.data = $jsonResult.data | Select-Object -First $Limit
                }
                if ($Raw) {
                    $jsonResult.data
                } else {
                    $jsonResult.data | Select-Object -ExcludeProperty "site_id","key","msg","_id","time","is_negative" @{N="SiteName";E={$SiteName}},
                                                        @{N="SiteID";E={$_.site_id}},
                                                        @{N="Category";E={$_.subsystem}},
                                                        @{N="Date";E={$_.DateTime}},
                                                        @{N="EventType";E={$_.key}},
                                                        @{N="Message";E={$_.msg}},
                                                        *
                    
                }
            }

        } catch {
            Write-Error "Something went wrong while fetching sites ($($_.Exception))" -ErrorAction Stop
        }
        
    }
}
#endregion

#region Device Management
<# FYI: device states:
    1: connected
    2: ?
    3: ?
    4: updating
    5: provisioning
    ?: ?
#>

function Get-UnifiDevice {
    <#
    .SYNOPSIS
        Gets Unifi Devices (AP, Switch, Gateways, etc.)
    .DESCRIPTION
        Gets Unifi Devices (AP, Switch, Gateways, etc.).
         
        You can pipe the output from "Get-UnifiSite" to this cmdlet
    .EXAMPLE
        PS C:\> Get-UnifiDevice -SiteName "default"
        Returns all devices from site "default"
    .EXAMPLE
        PS C:\> Get-UnifiSite * | Get-UnifiDevice
        Returns all devices from all sites
    .OUTPUTS
        Returns JSON-Data
    #>

    [CmdletBinding()]
    [OutputType([Object])]

    param(
        # Name of the site (Unifi's internal name is used, not the name visible in the web interface)
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true )]
        [String]
        $SiteName,

        # Do not filter or rename output, just sent the json result back as raw data
        [Parameter(Mandatory = $false)]
        [switch]
        $Raw
    )
   
    process {
        try {
            $jsonResult = Invoke-UnifiRestCall -Method GET -Route "api/s/$($SiteName)/stat/device"

            if ($jsonResult.meta.rc -eq "ok") {

                if ($Raw) {
                    $jsonResult.data
                } else {
                    $jsonResult.data | Select-Object    @{N="SiteName";E={$SiteName}},
                                                        @{N="SiteID";E={$_.site_id}},
                                                        Adopted,
                                                        @{N="InformIP";E={$_.inform_ip}},
                                                        @{N="InformURL";E={$_.inform_url}},
                                                        IP,
                                                        MAC,
                                                        Model,
                                                        @{N="Name";E={ if (!$_.name){ $_.MAC}else{$_.name} }},
                                                        Serial,
                                                        Version,
                                                        @{N="State";E={
                                                            $upgradestate = $_.upgrade_state
                                                            switch($_.state) {
                                                                1 { "Connected" }
                                                                4 { 
                                                                    switch ($upgradestate) {
                                                                        3 { "Updating (Downloading)" }
                                                                        5 { "Updating (Writing)" }
                                                                        default { "Updating" }
                                                                    }
                                                                }
                                                                5 { "Provisioning" }
                                                                default { $_.state }
                                                            }
                                                        }},
                                                        @{N="Connected";E={ (Get-Date("1970-01-01 00:00:00")).AddSeconds($_.connected_at) }},
                                                        @{N="Provisioned";E={ (Get-Date("1970-01-01 00:00:00")).AddSeconds($_.provisioned_at) }},
                                                        @{N="LastSeen";E={ (Get-Date("1970-01-01 00:00:00")).AddSeconds($_.last_seen) }},
                                                        @{N="Uptime";E={ [Timespan]::FromSeconds($_.Uptime).ToString() }},
                                                        @{N="Startup";E={ (Get-Date("1970-01-01 00:00:00")).AddSeconds($_.startup_timestamp) }},
                                                        @{N="UpdateAvailable";E={ $_.upgradable }},
                                                        @{N="UpdateableFirmware";E={ $_.upgrade_to_firmware }},
                                                        @{N="Load1";E={ $_.sys_stats.loadavg_1 }},
                                                        @{N="Load5";E={ $_.sys_stats.loadavg_5 }},                                                        
                                                        @{N="Load15";E={ $_.sys_stats.loadavg_15 }},
                                                        @{N="CPUUsed";E={ $_."system-stats".cpu }},
                                                        @{N="MemUsed";E={ $_."system-stats".mem }}
                }
            }

        } catch {
            Write-Error "Something went wrong while fetching devices ($($_.Exception))" -ErrorAction Stop
        }
    }
}

function Restart-UnifiDevice {
    <#
    .SYNOPSIS
        Restarts a unifi device
    .DESCRIPTION
        Restarts a unifi device. Use the $Force-Switch to skip asking for confirmation
 
        You can pipe the output from "Get-UnifiDevice" to this cmdlet
    .EXAMPLE
        PS C:\> Get-UnifiSite "Test" | Get-UnifiDevice "AP01" | Restart-UnifiDevice
        Restarts the device with the name "AP01" in site "Test", but asks for confirmation
    .EXAMPLE
        PS C:\> Restart-UnifiDevice -SiteName "Test" -MAC "00:11:22:33:44:55" -Force
        Restarts the device with the mac "00:11:22:33:44:55" in site "Test" and does not ask for confirmation
    .OUTPUTS
        Returns $True on Success
        Returns $False on Failure
    #>

    [CmdletBinding()]
    [OutputType([Boolean])]

    param(
        # Name of the site (Unifi's internal name is used, not the name visible in the web interface)
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true )]
        [String]
        $SiteName,

        # MAC of the device to restart
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true )]
        [String]
        $MAC,

        # Do not filter or rename output, just sent the json result back as raw data
        [Parameter(Mandatory = $false)]
        [switch]
        $Raw,

        # Do not ask for confirmation
        [Parameter(Mandatory = $false)]
        [switch]
        $Force
    )
   
    process {
        
        try {
            if (!$Force) {
                do {
                    $answer = Read-Host -Prompt "Do you really want to restart the device '$MAC'? (y/N): "
                } while($answer -ne "y" -and $answer -ne "n" -and $answer -ne "")

                if ($answer -eq "" -or $answer -eq "n") {
                    Write-Verbose "Restart of device '$MAC' was aborted by user"
                    return $null
                }

            }
            $jsonResult = Invoke-UnifiRestCall -Method POST -Route "api/s/$($SiteName)/cmd/devmgr" -Body (@{cmd = "restart"; mac = $MAC; reboot_type = "soft"} | ConvertTo-JSON)

            if ($jsonResult.meta.rc -eq "ok") {
                Write-Verbose "Device with MAC '$MAC' will reboot now"
                return $True
            } else {
                Write-Error "Could not reboot device mit MAC '$MAC'"
                return $False
            }

        } catch {
            Write-Error "Something went wrong while rebooting device with MAC '$MAC' ($($_.Exception))" -ErrorAction Stop
        }
        
    }
}

function Sync-UnifiDevice {
    <#
    .SYNOPSIS
        Syncs a unifi device with the unifi controller (will force a provisioning)
    .DESCRIPTION
        Syncs a unifi device with the unifi controller (will force a provisioning)
 
        You can pipe the output from "Get-UnifiDevice" to this cmdlet
    .EXAMPLE
        PS C:\> Get-UnifiSite "Test" | Get-UnifiDevice "AP01" | Sync-UnifiDevice
        Provisions the device with the name "AP01" in site "Test"
    .EXAMPLE
        PS C:\> Sync-UnifiDevice -SiteName "Test" -MAC "00:11:22:33:44:55"
        Provisions the device with the mac "00:11:22:33:44:55" in site "Test"
    .OUTPUTS
        Returns $True on Success
        Returns $False on Failure
    #>

    [CmdletBinding()]
    [OutputType([Boolean])]

    param(
        # Name of the site (Unifi's internal name is used, not the name visible in the web interface)
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true )]
        [String]
        $SiteName,

        # MAC of the device to sync
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true )]
        [String]
        $MAC,

        # Do not filter or rename output, just sent the json result back as raw data
        [Parameter(Mandatory = $false)]
        [switch]
        $Raw
    )
   
    process {
        
        try {
            $jsonResult = Invoke-UnifiRestCall -Method POST -Route "api/s/$($SiteName)/cmd/devmgr" -Body (@{cmd = "force-provision"; mac = $MAC} | ConvertTo-JSON)

            if ($jsonResult.meta.rc -eq "ok") {
                Write-Verbose "Device with '$MAC' will force a provision"
                return $True
            } else {
                Write-Error "Could not force a provision for device with MAC '$MAC'"
                return $False
            }

        } catch {
            Write-Error "Something went wrong while fetching sites ($($_.Exception))" -ErrorAction Stop
        }
        
    }
}

function Update-UnifiDevice {
    <#
    .SYNOPSIS
        Installs/updates the firmware of a unifi device
    .DESCRIPTION
        Installs/updates the firmware of a unifi device.
 
        If you omit the $URL-Parameter the device will receive the latest firmware from the unifi controller.
 
        You can pipe the output from "Get-UnifiDevice" to this cmdlet
    .EXAMPLE
        PS C:\> Get-UnifiSite "Test" | Get-UnifiDevice "AP01" | Update-UnifiDevice
        Updates the device with the name "AP01" in site "Test"
    .EXAMPLE
        PS C:\> Update-UnifiDevice -SiteName "Test" -MAC "00:11:22:33:44:55" -URL "https://dl.ui.com/unifi/firmware/U7HD/5.43.56.12784/BZ.ipq806x_5.43.56+12784.211209.2339.bin"
        Installs the firmware found in $URL to the device with the mac "00:11:22:33:44:55" in site "Test"
    .OUTPUTS
        Returns $True on Success
        Returns $False on Failure
    #>

    [CmdletBinding()]
    [OutputType([Boolean])]

    param(
        # Name of the site (Unifi's internal name is used, not the name visible in the web interface)
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true )]
        [String]
        $SiteName,

        # MAC of the device to update
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true )]
        [String]
        $MAC,

        # URL of the custom update
        [Parameter(Mandatory = $false )]
        [String]
        $URL,

        # Wait for the update to complete before returning
        [Parameter(Mandatory = $false )]
        [Switch]
        $Wait,

        # Time in seconds to wait for the update to finish. Default is 300 seconds (5 minutes)
        [Parameter(Mandatory = $false )]
        [int]
        $WaitTimeout = 300,

        # Forcing the update (do not check device state before initiating the update and do not ask for confirmation)
        [Parameter(Mandatory = $false )]
        [Switch]
        $Force
    )
   
    process {
        
        try {

            $device = Get-UnifiDevice -SiteName $SiteName | Where-Object { $_.MAC -eq $MAC }
            if ($device) {
                if ($device.UpdateAvailable -eq $True -or $URL) {
                    if ($device.state -eq "Connected") {
                        if (!$Force) {
                            do {
                                if ($URL) {
                                    $answer = Read-Host -Prompt "Do you really want to update the device '$($device.Name)' with the firmware from URL '$URL'? (y/N): "
                                } else {
                                    $answer = Read-Host -Prompt "Do you really want to update the device '$($device.Name)'' to firmware '$($device.UpdateableFirmware)'? (y/N): "
                                }
                            } while($answer -ne "y" -and $answer -ne "n" -and $answer -ne "")
            
                            if ($answer -eq "" -or $answer -eq "n") {
                                Write-Verbose "Update of device '$($device.Name)' was aborted by user"
                                return $False
                            }
            
                        }
                        if ($URL) {
                            $jsonResult = Invoke-UnifiRestCall -Method POST -Route "api/s/$($SiteName)/cmd/devmgr" -Body (@{cmd = "upgrade-external"; mac = $MAC; url = $URL} | ConvertTo-JSON)
                        } else {
                            $jsonResult = Invoke-UnifiRestCall -Method POST -Route "api/s/$($SiteName)/cmd/devmgr" -Body (@{cmd = "upgrade"; mac = $MAC } | ConvertTo-JSON)
                        }

                        if ($jsonResult.meta.rc -eq "ok") {
                            Write-Verbose "Device '$($device.Name)' is updating"

                            $now = Get-Date
                            if ($Wait) {
                                do {
                                    $device = Get-UnifiDevice -SiteName $SiteName | Where-Object { $_.MAC -eq $MAC }
                                    Write-Verbose "Device '$($Device.Name)' is still updating. Checking again in 30 seconds"
                                    Start-Sleep -Seconds 30
                                    if ( ((Get-Date) - $now).TotalSeconds -gt $WaitTimeout ) {
                                        Write-Warning "Wait-Timeout ($WaitTimeout seconds) reached. Do not wait any longer for the update to finish..."
                                        break
                                    }
                                } while ($device.State -like "Updating*")
                            }
                            return $True
                        } else {
                            Write-Error "Device '$($device.Name)' could not do a firmware update from URL '$URL'"
                            return $False
                        }
                    } else {
                        Write-Error "Device '$($device.Name)' is not in 'connected'-state"
                        return $False
                    }
                } else {
                    Write-Error "Device '$($device.Name)' already has the latest firmware installed"
                    return $False
                }
            } else {
                Write-Error "Device with MAC '$MAC' was not found in site '$SiteName'"
                return $False
            }

        } catch {
            Write-Error "Something went wrong while doing a firmware update for Device with MAC '$MAC' ($($_.Exception))" -ErrorAction Stop
            return $False
        }        
    }
}

function Edit-UnifiDevice { # not tested yet
    <#
    .SYNOPSIS
        Edits a unifi device (access point, gateway, switch, etc)
    .DESCRIPTION
        TODO
    .EXAMPLE
        PS C:\> Edit-UnifiDevice TODO
    .EXAMPLE
        PS C:\> TODO
    #>

    [CmdletBinding()]
    [OutputType([Object])]

    param(
        # Name of the site (Unifi's internal name is used, not the name visible in the web interface)
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true )]
        [String]
        $SiteName,

        # Do not filter or rename output, just sent the json result back as raw data
        [Parameter(Mandatory = $false)]
        [switch]
        $Raw,

        # ID of the Device to be edited
        [Parameter(
            Mandatory = $true,
            ValueFromPipelineByPropertyName = $true
        )]
        [ValidateNotNullOrEmpty()]
        [string]
        $DeviceID,

        # New name of the device. Leave empty to keep the name
        [Parameter(
            Mandatory = $false,
            ValueFromPipelineByPropertyName = $true
        )]
        [ValidateNotNullOrEmpty()]
        [string]
        $DeviceName
    )
   
    process {
        
        try {
            $Device = Get-UnifiDevice -SiteName $SiteName | Where-Object { $_.DeviceID -eq $DeviceID }

            if ($Device) {

                # Use current name if no new name was given
                if ([String]::IsNullOrWhiteSpace($DeviceName)) {
                    $DeviceName = $Device.DeviceName
                }

                $Body = @{
                    #'_id' = $Device.GroupID
                    #'site_id' = $Device.SiteID
                    name = $GroupName
                    #group_type = $Device.GroupType
                    #group_members = $GroupMembers
                } | ConvertTo-Json
                
                $jsonResult = Invoke-UnifiRestCall -Method PUT -Route "api/s/$($siteName)/rest/device/$($Device.DeviceID)" -Body $Body 

                if ($jsonResult.meta.rc -eq "ok") {
                    Write-Verbose "Device '$DeviceName' was successfully edited for site '$SiteName'"
                    
                    if ($Raw) {
                        $jsonResult.data
                    } else {
                        $jsonResult.data <# | Select-Object @{N="SiteName";E={$SiteName}},
                                                            @{N="SiteID";E={$_.site_id}},
                                                            @{N="GroupID";E={$_._id}},
                                                            @{N="GroupName";E={$_.name}},
                                                            @{N="GroupMembers";E={$_.group_members}},
                                                            @{N="GroupType";E={$_.group_type}} #>

                    }
                } else {
                    Write-Error "Device '$DeviceName' was NOT edited for site '$SiteName' -> error: $($jsonResult.meta.msg)"
                }
            } else {
                Write-Error "No Device with ID '$DeviceID' was found in site '$SiteName'"
            }

        } catch {
            Write-Warning "Something went wrong while editing device with ID '$DeviceID' for site $SiteName ($_)"
        }
        
    }
}
#endregion

#region Client Management
function Get-UnifiClient {
    <#
    .SYNOPSIS
        Gets Unifi Clients (Users, Guests)
    .DESCRIPTION
        Gets Unifi Clients (Users, Guests)
         
        You can pipe the output from "Get-UnifiSite" to this cmdlet
 
        This function lists all known clients by default. If you wish to only show active (currently connected) clients/users use the $Active-Switch
 
        THe output of Active clients differs from the output of all clients.
    .EXAMPLE
        PS C:\> Get-UnifiClient -SiteName "default"
        Returns all clients from site "default"
    .EXAMPLE
        PS C:\> Get-UnifiClient -SiteName "default" -Active
        Returns all currently connected clients from site "default"
    .OUTPUTS
        Returns JSON-Data
    #>

    [CmdletBinding()]
    [OutputType([Object])]

    param(
        # Name of the site (Unifi's internal name is used, not the name visible in the web interface)
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true )]
        [String]
        $SiteName,

        # Do not filter or rename output, just sent the json result back as raw data
        [Parameter(Mandatory = $false)]
        [switch]
        $Raw,

        # Only list active clients and show additional info for them
        [Parameter(Mandatory = $false)]
        [switch]
        $Active
    )
   
    process {
        try {
            if ($Active) {
                $jsonResult = Invoke-UnifiRestCall -Method GET -Route "api/s/$($SiteName)/stat/sta"

                if ($jsonResult.meta.rc -eq "ok") {

                    if ($Raw) {
                        $jsonResult.data
                    } else {
                        $jsonResult.data | Select-Object    @{N="SiteName";E={$SiteName}},
                                                            @{N="SiteID";E={$_.site_id}},
                                                            MAC,
                                                            @{N="IPAddress";E={$_.ip}},
                                                            VLAN,
                                                            @{N="Username";E={$_."1x_identity"}},
                                                            Hostname,
                                                            @{N="Manufacturer";E={$_.oui}},
                                                            @{N="Guest";E={$_.is_guest}},
                                                            @{N="Wired";E={$_.is_wired}},
                                                            @{N="SSID";E={$_.essid}},
                                                            @{N="BSSID";E={$_.bssid}},
                                                            @{N="AccessPointMAC";E={$_.ap_mac}},
                                                            Channel,
                                                            Radio,
                                                            Signal,
                                                            Noise,
                                                            RSSI,
                                                            @{N="TXRate";E={ "$($_.tx_rate / 1000) Mbps"}},
                                                            @{N="RXRate";E={ "$($_.rx_rate / 1000) Mbps" }},
                                                            @{N="TXPower";E={$_.tx_power}},
                                                            @{N="WifiTX";E={ "$($_.tx_bytes / 1048576) MB" }},
                                                            @{N="WifiRX";E={ "$($_.rx_bytes / 1048576) MB" }},
                                                            @{N="WiredTX";E={ "$($_.wired_tx_bytes / 1048576) MB" }},
                                                            @{N="WiredRX";E={ "$($_.wired_rx_bytes / 1048576) MB" }},
                                                            @{N="TXAttempts";E={$_.wifi_tx_attempts}},
                                                            @{N="TXRetries";E={$_.tx_retries}},
                                                            Authorized,
                                                            @{N="Uptime";E={ [Timespan]::FromSeconds($_.Uptime).ToString() }},
                                                            @{N="FirstSeen";E={ (Get-Date("1970-01-01 00:00:00")).AddSeconds($_.first_seen) }},
                                                            @{N="LastSeen";E={ (Get-Date("1970-01-01 00:00:00")).AddSeconds($_.last_seen) }},
                                                            @{N="Disconnected";E={ (Get-Date("1970-01-01 00:00:00")).AddSeconds($_.disconnect_timestamp) }},
                                                            @{N="Associated";E={ (Get-Date("1970-01-01 00:00:00")).AddSeconds($_.assoc_time) }},
                                                            @{N="AssociatedLatest";E={ (Get-Date("1970-01-01 00:00:00")).AddSeconds($_.latest_assoc_time) }}
                    }
                }
            } else {
                $jsonResult = Invoke-UnifiRestCall -Method GET -Route "api/s/$($SiteName)/rest/user"

                if ($jsonResult.meta.rc -eq "ok") {

                    if ($Raw) {
                        $jsonResult.data
                    } else {
                        $jsonResult.data | Select-Object    @{N="SiteName";E={$SiteName}},
                                                            @{N="SiteID";E={$_.site_id}},
                                                            MAC,
                                                            @{N="Manufacturer";E={$_.oui}},
                                                            @{N="Guest";E={$_.is_guest}},
                                                            @{N="Wired";E={$_.is_wired}},
                                                            @{N="FirstSeen";E={ (Get-Date("1970-01-01 00:00:00")).AddSeconds($_.first_seen) }},
                                                            @{N="LastSeen";E={ (Get-Date("1970-01-01 00:00:00")).AddSeconds($_.last_seen) }},
                                                            @{N="Disconnected";E={ (Get-Date("1970-01-01 00:00:00")).AddSeconds($_.disconnect_timestamp) }}
                    }
                }
            }
        } catch {
            Write-Error "Something went wrong while fetching clients ($($_.Exception))"
        }
    }
}

function Disconnect-UnifiClient {
    <#
    .SYNOPSIS
        Disconnects a unifi client device (the client will try to reconnect)
    .DESCRIPTION
        Disconnects a unifi client device (the client will try to reconnect). This function will ask for confirmation unless the $Force-Switch is used
 
        You can pipe the output from "Get-UnifiClient" to this cmdlet
    .EXAMPLE
        PS C:\> Get-UnifiSite "Test" | Get-UnifiDevice "iPad01" | Disconnect-UnifiClient
        Reconnects the client with the name "iPad01" in site "Test"
    .EXAMPLE
        PS C:\> Disconnect-UnifiClient -SiteName "Test" -MAC "00:11:22:33:44:55"
        Reconnects the client with the mac "00:11:22:33:44:55" in site "Test"
    .OUTPUTS
        Returns $True on Success
        Returns $False on Failure
    #>

    [CmdletBinding()]
    [OutputType([boolean])]

    param(
        # Name of the site (Unifi's internal name is used, not the name visible in the web interface)
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true )]
        [String]
        $SiteName,

        # MAC of the client to reconnect
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true )]
        [String]
        $MAC,

        # Do not filter or rename output, just sent the json result back as raw data
        [Parameter(Mandatory = $false)]
        [switch]
        $Raw,

        # Do not ask for confirmation
        [Parameter(Mandatory = $false)]
        [switch]
        $Force
    )
   
    process {
        
        try {
            if (!$Force) {
                do {
                    $answer = Read-Host -Prompt "Do you really want to disconnect the client '$MAC'? (y/N): "
                } while($answer -ne "y" -and $answer -ne "n" -and $answer -ne "")

                if ($answer -eq "" -or $answer -eq "n") {
                    Write-Verbose "Disconnecting the client '$MAC' was aborted by user"
                    return $null
                }

            }
            $jsonResult = Invoke-UnifiRestCall -Method POST -Route "api/s/$($SiteName)/cmd/stamgr" -Body (@{cmd = "kick-sta"; mac = $MAC} | ConvertTo-JSON)

            if ($jsonResult.meta.rc -eq "ok") {
                Write-Verbose "Client '$MAC' was disconnected"
            } else {
                Write-Error "Client '$MAC' was NOT disconnected"
            }

        } catch {
            Write-Error "Something went wrong while disconnecting the client with the MAC '$MAC' ($($_.Exception))" -ErrorAction Stop
        }
        
    }
}
#endregion

#region Firewall Management
function Get-UnifiFirewallGroup {
    <#
    .SYNOPSIS
        Lists firewall groups in a site
    .DESCRIPTION
        Lists firewall groups in a site.
        A firewall group can be a group of ports, ipv4-addresses or ipv6-addresses. This group is then used in a firewall rule
    .EXAMPLE
        PS C:\> Get-UnifiFirewallGroup -SiteName "default"
        Lists the firewall groups for the default site
    .OUTPUTS
        Returns JSON-Data
    #>

    [CmdletBinding()]
    [OutputType([Object])]

    param(
        # Name of the site (Unifi's internal name is used, not the name visible in the web interface)
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true )]
        [String]
        $SiteName,

        # Do not filter or rename output, just sent the json result back as raw data
        [Parameter(Mandatory = $false)]
        [switch]
        $Raw
    )
   
    process {
        try {
            $jsonResult = Invoke-UnifiRestCall -Method GET -Route "api/s/$($SiteName)/rest/firewallgroup"

            if ($jsonResult.meta.rc -eq "ok") {

                if ($Raw) {
                    $jsonResult.data
                } else {
                    $jsonResult.data | Select-Object    @{N="SiteName";E={$SiteName}},
                                                        @{N="SiteID";E={$_.site_id}},
                                                        @{N="GroupID";E={$_._id}},
                                                        @{N="GroupName";E={$_.name}},
                                                        @{N="GroupMembers";E={$_.group_members}},
                                                        @{N="GroupType";E={$_.group_type}}
                }
            }
        } catch {
            Write-Warning "Something went wrong while fetching firewall groups for site $($SiteName) ($_)"
        }        
    }
}

function New-UnifiFirewallGroup {
    <#
    .SYNOPSIS
        Creates a new firewall group in a site
    .DESCRIPTION
        Creates a new firewall group in a site
 
        Ports can be separated by a comma (20,21,22) and/or specified as a range (5900-5910)
        IP-Address can also be separated by a comma and/or specified as a network address (10.0.0.0/8)
    .EXAMPLE
        PS C:\> New-UnifiFirewallGroup -SiteName "default" -GroupName "FTP-Ports" -GroupMembers 20,21 -GroupType port-group
        Creates the firewall group "FTP-Ports" in the default site as a "port-group" and assigns the ports 20&21 to it
    .EXAMPLE
        PS C:\> Get-UnifiSite -SiteName "Production" | New-UnifiFirewallGroup -GroupName "Internal Networks" -GroupType "address-group" -GroupMembers "192.168.0.0/24","192.168.100.0/24"
        Creates the firewall group "Internal Networks" in the "Production" site as an "address-group" and assigns the networks 192.168.0.0/24 and 192.168.100.0/24 to it
    .OUTPUTS
        Returns JSON-Data for the newly created object
    #>

    [CmdletBinding()]
    [OutputType([Object])]

    param(
        # Name of the site (Unifi's internal name is used, not the name visible in the web interface)
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true )]
        [String]
        $SiteName,

        # Do not filter or rename output, just sent the json result back as raw data
        [Parameter(Mandatory = $false)]
        [switch]
        $Raw,

        # Name of the Firewall group to be created
        [Parameter(
            Mandatory = $true
        )]
        [ValidateNotNullOrEmpty()]
        [string]
        $GroupName,

        # Type of the Firewall group to be created (one of "address-group","ipv6-address-group","port-group")
        [Parameter(
            Mandatory = $true
        )]
        [ValidateSet("address-group","ipv6-address-group","port-group")]
        [string]
        $GroupType,

        # Group members (can be ipv4/ipv6 addresses or port numbers/ranges). Can also be empty
        [Parameter(
            Mandatory = $false
        )]
        [string[]]
        $GroupMembers = @()
    )
   
    process {
        try {

            $Body = @{
                name = $GroupName
                group_type = $GroupType
                group_members = $GroupMembers
            } | ConvertTo-Json

            $jsonResult = Invoke-UnifiRestCall -Method POST -Route "api/s/$($SiteName)/rest/firewallgroup" -Body $Body

            if ($jsonResult.meta.rc -eq "ok") {
                Write-Verbose "Firewall group '$GroupName' successfully created for site '$SiteName'"

                if ($Raw) {
                    $jsonResult.data
                } else {
                    $jsonResult.data | Select-Object    @{N="SiteName";E={$SiteName}},
                                                        @{N="SiteID";E={$_.site_id}},
                                                        @{N="FirewallGroupID";E={$_._id}},
                                                        @{N="FirewallGroupName";E={$_.name}},
                                                        @{N="FirewallGroupMembers";E={$_.group_members}},
                                                        @{N="FirewallGroupType";E={$_.group_type}}

                }
            } else {
                if ($jsonResult.meta.msg -eq "api.err.FirewallGroupExisted") {
                    Write-Warning "Firewall group '$GroupName' already exists in site '$SiteName'"
                } else {
                    Write-Error "Firewall group '$GroupName' was NOT created for site '$SiteName'"
                }
            }

        } catch {
            Write-Warning "Something went wrong while creating a new firewall group for site $($SiteName) ($_)"
        }
    }
}

function Edit-UnifiFirewallGroup {
    <#
    .SYNOPSIS
        Edits a firewall group for a site
    .DESCRIPTION
        Edits a firewall group for a site
 
        You can only change the name and the members of the firewall group, but you cannot change the group-type
 
        Leave the name or the members empty to keep them
    .EXAMPLE
        PS C:\> Get-UnifiSite "default" | Get-UnifiFirewallGroup -GroupName "FTP-Ports" | Edit-UnifiFirewallGroup -GroupName "RDP-Ports" -GroupMembers 3389
        Changes the name and the ports of the firewall group "FTP-Ports" in the default site
    .OUTPUTS
        Returns JSON-Data
    #>

    [CmdletBinding()]
    [OutputType([Object])]

    param(
        # Name of the site (Unifi's internal name is used, not the name visible in the web interface)
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true )]
        [String]
        $SiteName,

        # Do not filter or rename output, just sent the json result back as raw data
        [Parameter(Mandatory = $false)]
        [switch]
        $Raw,

        # ID of the Firewall group to be edited
        [Parameter(
            Mandatory = $true,
            ValueFromPipelineByPropertyName = $true
        )]
        [ValidateNotNullOrEmpty()]
        [string]
        $GroupID,

        # New name of the group. Leave empty to keep the name
        [Parameter(
            Mandatory = $false,
            ValueFromPipelineByPropertyName = $true
        )]
        [ValidateNotNullOrEmpty()]
        [string]
        $GroupName,

        # Group members (can be ipv4/ipv6 addresses or port numbers/ranges). Can also be empty. All members will be overridden by this parameter
        [Parameter(
            Mandatory = $false
        )]
        [string[]]
        $GroupMembers = @()
    )
   
    process {
        
        try {
            $fwGroup = Get-UnifiFirewallGroup -SiteName $SiteName | Where-Object { $_.GroupID -eq $GroupID }

            if ($fwGroup) {

                if ( $GroupName -eq $fwGroup.GroupName -and $GroupMembers -eq $fwGroup.GroupMembers) {
                    Write-Warning "Nothing has changed"
                } else {
                    # Use current name if no new name was given
                    if ([String]::IsNullOrWhiteSpace($GroupName)) {
                        $GroupName = $fwGroup.GroupName
                    }

                    # Use current members if no new members were given
                    if ([String]::IsNullOrWhiteSpace($GroupMembers)) {
                        $GroupMembers = $fwGroup.GroupMembers
                    }

                    $Body = @{
                        '_id' = $fwGroup.GroupID
                        'site_id' = $fwGroup.SiteID
                        name = $GroupName
                        group_type = $fwGroup.GroupType
                        group_members = @($GroupMembers)
                    } | ConvertTo-Json
                    
                    $jsonResult = Invoke-UnifiRestCall -Method PUT -Route "api/s/$($siteName)/rest/firewallgroup/$($fwGroup.GroupID)" -Body $Body

                    if ($jsonResult.meta.rc -eq "ok") {
                        Write-Verbose "Firewall group '$GroupName' successfully edited for site '$SiteName'"
                        
                        if ($Raw) {
                            $jsonResult.data
                        } else {
                            $jsonResult.data | Select-Object    @{N="SiteName";E={$SiteName}},
                                                                @{N="SiteID";E={$_.site_id}},
                                                                @{N="GroupID";E={$_._id}},
                                                                @{N="GroupName";E={$_.name}},
                                                                @{N="GroupMembers";E={$_.group_members}},
                                                                @{N="GroupType";E={$_.group_type}}
                        }
                    } else {
                        Write-Error "Firewall group '$GroupName' was NOT edited for site '$SiteName' -> error: $($jsonResult.meta.msg)"
                    }
                }
            } else {
                Write-Error "No Firewall Group with ID '$GroupID' in site '$SiteName' was found"
            }

        } catch {
            Write-Warning "Something went wrong while editing firewall group with ID '$GroupID' for site $SiteName ($_)"
        }        
    }
}

function Remove-UnifiFirewallGroup {
    <#
    .SYNOPSIS
        Deletes a firewall group in a site
    .DESCRIPTION
        Deletes a firewall group in a site and asks for confirmation
    .EXAMPLE
        PS C:\> Get-UnifiSite "default" | Get-UnifiFirewallGroup -GroupName "FTP-Ports" | Remove-UnifiFirewallGroup -Force
        Removes the firewall group "FTP-Ports" in the "default" site and skips confirmation
    .OUTPUTS
        Returns $True on Success
        Returns $False on Failure
    #>

    [CmdletBinding()]
    [OutputType([Boolean])]

    param(
        # Name of the site (Unifi's internal name is used, not the name visible in the web interface)
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true )]
        [String]
        $SiteName,

        # ID of the Firewall group to be deleted
        [Parameter(
            Mandatory = $true,
            ValueFromPipelineByPropertyName = $true
        )]
        [ValidateNotNullOrEmpty()]
        [string]
        $GroupID,

        # Do not ask for confirmation
        [Parameter(Mandatory = $false)]
        [switch]
        $Force
    )
   
    process {
        try {
            $fwGroup = Get-UnifiFirewallGroup -SiteName $SiteName | Where-Object { $_.GroupID -eq $GroupID }

            if ($fwGroup) {
                if (!$Force) {
                    do {
                        $answer = Read-Host -Prompt "Do you really want to delete the firewall group '$($fwGroup.GroupName)' (ID: $($GroupID))? (y/N): "
                    } while($answer -ne "y" -and $answer -ne "n" -and $answer -ne "")

                    if ($answer -eq "" -or $answer -eq "n") {
                        Write-Verbose "Deletion of firewall group '$($fwGroup.GroupName)' (ID: $($GroupID)) was aborted by user"
                        return $False
                    }

                }
                $jsonResult = Invoke-UnifiRestCall -Method DELETE -Route "api/s/$($SiteName)/rest/firewallgroup/$($GroupID)"

                if ($jsonResult.meta.rc -eq "ok") {
                    Write-Verbose "Firewall group '$($fwGroup.GroupName)' successfully deleted for site $SiteName"
                    return $True
                } else {
                    Write-Error "Firewall group '$($fwGroup.GroupName)' was NOT deleted for site $SiteName"
                    return $False
                }
            } else {
                Write-Error "No Firewall Group with '$GroupID' was found in site '$SiteName'"
                return $False
            }

        } catch {
            Write-Warning "Something went wrong while deleting the firewall group with ID '$GroupID' for site $($SiteName) ($_)"
            return $False
        }        
    }
}

function Get-UnifiFirewallRule {
    <#
    .SYNOPSIS
        Lists firewall rules in a site
    .DESCRIPTION
        Lists firewall rules in a site
    .EXAMPLE
        PS C:\> Get-UnifiSite "default" | Get-UnifiFirewallRule
        Lists all rules in the default site
    .OUTPUTS
        Returns JSON-Data
    #>

    [CmdletBinding()]
    [OutputType([Object])]

    param(
        # Name of the site (Unifi's internal name is used, not the name visible in the web interface)
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true )]
        [String]
        $SiteName,

        # Do not filter or rename output, just sent the json result back as raw data
        [Parameter(Mandatory = $false)]
        [switch]
        $Raw
    )
   
    process {
        try {
            $jsonResult = Invoke-UnifiRestCall -Method GET -Route "api/s/$($SiteName)/rest/firewallrule"

            if ($jsonResult.meta.rc -eq "ok") {

                if ($Raw) {
                    $jsonResult.data
                } else {
                    $jsonResult.data | Select-Object    @{N="SiteName";E={$SiteName}},
                                                        @{N="SiteID";E={$_.site_id}},
                                                        @{N="RuleName";E={$_.Name}},
                                                        @{N="RuleID";E={$_._id}},
                                                        @{N="RuleSet";E={$_.ruleset}},
                                                        @{N="Enabled";E={$_.enabled}},
                                                        @{N="Action";E={$_.action}},
                                                        @{N="DstAddress";E={$_.dst_address}},
                                                        @{N="DstFirewallGroupIDs";E={$_.dst_firewallgroup_ids}},
                                                        @{N="DstNetworkConfID";E={$_.dst_networkconf_id}},
                                                        @{N="DstNetworkConfType";E={$_.dst_networkconf_type}},
                                                        @{N="IcmpTypename";E={$_.icmp_typename}},
                                                        @{N="IPSEC";E={$_.ipsec}},
                                                        @{N="Logging";E={$_.logging}},
                                                        @{N="Protocol";E={$_.protocol}},
                                                        @{N="ProtocolMatchExcepted";E={$_.protocol_match_excepted}},
                                                        @{N="RuleIndex";E={$_.rule_index}},
                                                        @{N="SrcAddress";E={$_.src_address}},
                                                        @{N="SrcFirewallGroupIDs";E={$_.src_firewallgroup_ids}},
                                                        @{N="SrcMACAddress";E={$_.src_mac_address}},
                                                        @{N="SrcNetworkConfID";E={$_.src_networkconf_id}},
                                                        @{N="SrcNetworkConfType";E={$_.src_networkconf_type}},
                                                        @{N="StateEstablished";E={$_.state_established}},
                                                        @{N="StateInvalid";E={$_.state_invalid}},
                                                        @{N="StateNew";E={$_.state_new}},
                                                        @{N="StateRelated";E={$_.state_related}},
                                                        @{N="SettingPreference";E={$_.setting_preference}}
                }
            }

        } catch {
            Write-Warning "Something went wrong while fetching firewall rules for site $($SiteName) ($_)"
        }
    }
}

function New-UnifiFirewallRule {
    <#
    .SYNOPSIS
        Creates a new firewall rule for a site
    .DESCRIPTION
        Creates a new firewall rule for a site
    .EXAMPLE
        PS C:\> $RDPGroup = Get-UnifiSite "default" | Get-UnifiFirewallGroup | Where-Object { $_.GroupName -eq "RDP-Ports" }
        PS C:\> Get-UnifiSite "default" | New-UnifiFirewallRule -RuleName "Allow RDP-Traffic" -RuleSet WAN_IN -Action Accept -Enabled $True -Protocol tcp_udp -DestinationFirewallGroupIDs $RDPGroup.GroupID
        Allow RDP-Traffic in default site for ruleset WAN_IN
    .OUTPUTS
        Returns JSON-Data
    #>

    [CmdletBinding()]
    [OutputType([Object])]

    param(
        # Name of the site (Unifi's internal name is used, not the name visible in the web interface)
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true )]
        [String]
        $SiteName,

        # ID of the site
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true )]
        [String]
        $SiteID,

        # Do not filter or rename output, just sent the json result back as raw data
        [Parameter(Mandatory = $false)]
        [switch]
        $Raw,

        # Name of the Firewall rule to be created
        [Parameter(
            Mandatory = $true
        )]
        [ValidateNotNullOrEmpty()]
        [string]
        $RuleName,

        # RuleSet of the Firewall rule in which the rule shall be created
        [Parameter(
            Mandatory = $true
        )]
        [ValidateSet("WAN_IN","WAN_OUT","WAN_LOCAL","LAN_IN","LAN_OUT","LAN_LOCAL","GUEST_IN","GUEST_OUT","GUEST_LOCAL")]
        [string]
        $RuleSet,

        # Action of the Firewall rule
        [Parameter(
            Mandatory = $true
        )]
        [ValidateSet("Drop","Reject","Accept")]
        [string]
        $Action,

        # State of the Firewall rule
        [Parameter(
            Mandatory = $true
        )]
        [Alias("State")]
        [bool]
        $Enabled,

        # Protocol of the Firewall rule
        [Parameter(
            Mandatory = $false
        )]
        [ValidateSet("all","tcp","udp","tcp_udp","icmp")] # Protocol can also be specified by an integer, but this is not implemented here yet
        [string]
        $Protocol = "all",

        # Should be logged to a syslog server?
        [Parameter(
            Mandatory = $false
        )]
        [switch]
        $Logging,

        # Match new Packages?
        [Parameter(
            Mandatory = $false
        )]
        [switch]
        $StateNew = $false,

        # Match established Packages?
        [Parameter(
            Mandatory = $false
        )]
        [switch]
        $StateEstablished = $false,

        # Match invalid Packages?
        [Parameter(
            Mandatory = $false
        )]
        [switch]
        $StateInvalid = $false,

        # Match related Packages?
        [Parameter(
            Mandatory = $false
        )]
        [switch]
        $StateRelated = $false,

        # Match IPSEC Packages?
        [Parameter(
            Mandatory = $false
        )]
        [ValidateSet("","match-ipsec","none")]
        [string]
        $IPSEC = "",

        # Source Type
        [Parameter(
            Mandatory = $false
        )]
        [ValidateSet("NETv4","ADDRv4")] # Netv4 = "Address/Port-Group" in WebUI, needs Parameter "SourceFirewallGroupID" or leave empty for no source filtering; ADDRv4 = "Network" or "IP Address" in WebUI
        [string]
        $SourceType = "NETv4",

        # Source Firewall Groups, must be used with $SourceType = NETv4
        [Parameter(
            Mandatory = $false
        )]
        [string[]]
        $SourceFirewallGroupIDs = @(),

        # Source Network ID, must be used with $SourceType = ADDRv4
        [Parameter(
            Mandatory = $false
        )]
        [string]
        $SourceNetworkID = "",

        # Source Address, must be used with $SourceType = ADDRv4
        [Parameter(
            Mandatory = $false
        )]
        [string]
        $SourceAddress,

        # Destination Type
        [Parameter(
            Mandatory = $false
        )]
        [ValidateSet("NETv4","ADDRv4")] # Netv4 = "Address/Port-Group" in WebUI, needs Parameter "SourceFirewallGroupID" or leave empty for no source filtering; ADDRv4 = "Network" or "IP Address" in WebUI
        [string]
        $DestinationType = "NETv4",

        # Destination Firewall Groups, must be used with $DestinationType = NETv4
        [Parameter(
            Mandatory = $false
        )]
        [string[]]
        $DestinationFirewallGroupIDs = @(),

        # Destination Network ID, must be used with $DestinationType = ADDRv4
        [Parameter(
            Mandatory = $false
        )]
        [string]
        $DestinationNetworkID = "",

        # Destination Address, must be used with $DestinationType = ADDRv4
        [Parameter(
            Mandatory = $false
        )]
        [string]
        $DestinationAddress,

        # Rule Index, set to "append", "prepend" or any number
        [Parameter(
            Mandatory = $false
        )]
        [string]
        $RuleIndex = "append"

        # missing parameters by now: icmp_typename, src_mac_address, dst_mac_address, setting_preference, protocol_match_excepted

    )
   
    process {

        if ($RuleIndex -eq "append" -or $RuleIndex -eq "prepend") {
            # Get all current firewall rules for this $RuleSet to calculate the new RuleIndex
            $curRules = Get-UnifiFirewallRule -siteName $SiteName | Where-Object { $_.RuleSet -eq $RuleSet } | Sort-Object -Property RuleIndex

            if ($curRules.Count -eq 0) {
                $RuleIndexNr = 2000
            } elseif ($RuleIndex -eq "append") {
                $RuleIndexNr = ($CurRules | Select-Object -Last 1 -ExpandProperty RuleIndex) + 1
            } elseif ($RuleIndex -eq "prepend") {
                $RuleIndexNr = ($CurRules | Select-Object First 1 -ExpandProperty RuleIndex) - 1
            }
        } else {
            $RuleIndexNr = $RuleIndex
        }

        if ($RuleIndexNr -le 0) {
            Write-Error "Firewall Rule Index can't be zero or negative"
            return ""
        }

        try {
            $Body = @{
                action                  = $Action.ToLower()
                dst_address             = $DestinationAddress           # only when $DestinationType -eq ADDRv4
                dst_firewallgroup_ids   = $DestinationFirewallGroupIDs  # only when $DestinationType -eq NETv4
                dst_networkconf_id      = $DestinationNetworkID        # only when $DestinationType -eq ADDRv4
                dst_networkconf_type    = $DestinationType
                enabled                 = $Enabled
                icmp_typename           = ""
                ipsec                   = $IPSEC
                logging                 = $Logging.IsPresent
                name                    = $RuleName
                protocol                = $Protocol.ToLower()
                protocol_match_excepted = $False
                rule_index              = $RuleIndexNr
                ruleset                 = $RuleSet
                src_address             = $SourceAddress                # only when $DestinationType -eq ADDRv4
                src_firewallgroup_ids   = $SourceFirewallGroupIDs       # only when $DestinationType -eq NETv4
                src_mac_address         = ""
                src_networkconf_id      = $SourceNetworkID             # only when $DestinationType -eq ADDRv4
                src_networkconf_type    = $SourceType
                state_established       = $StateEstablished.IsPresent
                state_invalid           = $StateInvalid.IsPresent
                state_new               = $StateNew.IsPresent
                state_related           = $StateRelated.IsPresent
                site_id                 = $SiteID
                setting_preference      = "manual"
            } | ConvertTo-Json

            $jsonResult = Invoke-UnifiRestCall -Method POST -Route "api/s/$($SiteName)/rest/firewallrule" -Body $Body

            if ($jsonResult.meta.rc -eq "ok") {
                Write-Verbose "Firewall rule '$RuleName' successfully created for site $SiteName"

                if ($Raw) {
                    $jsonResult.data
                } else {
                    $jsonResult.data | Select-Object    @{N="SiteName";E={$SiteName}},
                                                        @{N="SiteID";E={$_.site_id}},
                                                        @{N="RuleName";E={$_.Name}},
                                                        @{N="RuleID";E={$_._id}},
                                                        @{N="RuleSet";E={$_.ruleset}},
                                                        @{N="Enabled";E={$_.enabled}},
                                                        @{N="Action";E={$_.action}},
                                                        @{N="DstAddress";E={$_.dst_address}},
                                                        @{N="DstFirewallGroupIDs";E={$_.dst_firewallgroup_ids}},
                                                        @{N="DstNetworkConfID";E={$_.dst_networkconf_id}},
                                                        @{N="DstNetworkConfType";E={$_.dst_networkconf_type}},
                                                        @{N="IcmpTypename";E={$_.icmp_typename}},
                                                        @{N="IPSEC";E={$_.ipsec}},
                                                        @{N="Logging";E={$_.logging}},
                                                        @{N="Protocol";E={$_.protocol}},
                                                        @{N="ProtocolMatchExcepted";E={$_.protocol_match_excepted}},
                                                        @{N="RuleIndex";E={$_.rule_index}},
                                                        @{N="SrcAddress";E={$_.src_address}},
                                                        @{N="SrcFirewallGroupIDs";E={$_.src_firewallgroup_ids}},
                                                        @{N="SrcMACAddress";E={$_.src_mac_address}},
                                                        @{N="SrcNetworkConfID";E={$_.src_networkconf_id}},
                                                        @{N="SrcNetworkConfType";E={$_.src_networkconf_type}},
                                                        @{N="StateEstablished";E={$_.state_established}},
                                                        @{N="StateInvalid";E={$_.state_invalid}},
                                                        @{N="StateNew";E={$_.state_new}},
                                                        @{N="StateRelated";E={$_.state_related}},
                                                        @{N="SettingPreference";E={$_.setting_preference}}
                }
            } else {
                Write-Error "Firewall rule '$RuleName' was NOT created for site '$SiteName'"
            }

        } catch {
            Write-Warning "Something went wrong while creating a new firewall rule for site $($SiteName) ($_)"
        }        
    }
}

function Edit-UnifiFirewallRule {
    <#
    .SYNOPSIS
        Edits a firewall rule in a site
    .DESCRIPTION
        TODO
    .EXAMPLE
        PS C:\> Edit-UnifiFirewallRule TODO
    .EXAMPLE
        PS C:\> TODO
    #>

    [CmdletBinding()]
    [OutputType([Object])]

    param(
        # Name of the site (Unifi's internal name is used, not the name visible in the web interface)
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true )]
        [String]
        $SiteName,

        # Do not filter or rename output, just sent the json result back as raw data
        [Parameter(Mandatory = $false)]
        [switch]
        $Raw,

        # ID of the Firewall group to be edited
        [Parameter(
            Mandatory = $true,
            ValueFromPipelineByPropertyName = $true
        )]
        [ValidateNotNullOrEmpty()]
        [string]
        $RuleID,

        # Name of the Firewall rule to be edited
        [Parameter(
            Mandatory = $false
        )]
        [ValidateNotNullOrEmpty()]
        [string]
        $RuleName,

        # RuleSet of the Firewall rule in which the rule shall be edited
        [Parameter(
            Mandatory = $false
        )]
        [ValidateSet("WAN_IN","WAN_OUT","WAN_LOCAL","LAN_IN","LAN_OUT","LAN_LOCAL","GUEST_IN","GUEST_OUT","GUEST_LOCAL")]
        [string]
        $RuleSet,

        # Action of the Firewall rule
        [Parameter(
            Mandatory = $false
        )]
        [ValidateSet("Drop","Reject","Accept")]
        [string]
        $Action,

        # State of the Firewall rule
        [Parameter(
            Mandatory = $false
        )]
        [Alias("State")]
        [bool]
        $Enabled,

        # Protocol of the Firewall rule
        [Parameter(
            Mandatory = $false
        )]
        [ValidateSet("all","tcp","udp","tcp_udp","icmp")] # Protocol can also be specified by an integer, but this is not implemented here yet
        [string]
        $Protocol,

        # Should be logged o a syslog server?
        [Parameter(
            Mandatory = $false
        )]
        [switch]
        $Logging,

        # Match new Packages?
        [Parameter(
            Mandatory = $false
        )]
        [switch]
        $StateNew,

        # Match established Packages?
        [Parameter(
            Mandatory = $false
        )]
        [switch]
        $StateEstablished,

        # Match invalid Packages?
        [Parameter(
            Mandatory = $false
        )]
        [switch]
        $StateInvalid,

        # Match related Packages?
        [Parameter(
            Mandatory = $false
        )]
        [switch]
        $StateRelated,

        # Match IPSEC Packages?
        [Parameter(
            Mandatory = $false
        )]
        [ValidateSet("","match-ipsec","none")]
        [string]
        $IPSEC,

        # Source Type
        [Parameter(
            Mandatory = $false
        )]
        [ValidateSet("NETv4","ADDRv4")] # Netv4 = "Address/Port-Group" in WebUI, needs Parameter "SourceFirewallGroupID" or leave empty for no source filtering; ADDRv4 = "Network" or "IP Address" in WebUI
        [string]
        $SourceType,

        # Source Firewall Groups, must be used with $SourceType = NETv4
        [Parameter(
            Mandatory = $false
        )]
        [string[]]
        $SourceFirewallGroupIDs,

        # Source Network ID, must be used with $SourceType = ADDRv4
        [Parameter(
            Mandatory = $false
        )]
        [string]
        $SourceNetworkID,

        # Source Address, must be used with $SourceType = ADDRv4
        [Parameter(
            Mandatory = $false
        )]
        [string]
        $SourceAddress,

        # Destination Type
        [Parameter(
            Mandatory = $false
        )]
        [ValidateSet("NETv4","ADDRv4")] # Netv4 = "Address/Port-Group" in WebUI, needs Parameter "SourceFirewallGroupID" or leave empty for no source filtering; ADDRv4 = "Network" or "IP Address" in WebUI
        [string]
        $DestinationType,

        # Destination Firewall Groups, must be used with $DestinationType = NETv4
        [Parameter(
            Mandatory = $false
        )]
        [string[]]
        $DestinationFirewallGroupIDs,

        # Destination Network ID, must be used with $DestinationType = ADDRv4
        [Parameter(
            Mandatory = $false
        )]
        [string]
        $DestinationNetworkID,

        # Destination Address, must be used with $DestinationType = ADDRv4
        [Parameter(
            Mandatory = $false
        )]
        [string]
        $DestinationAddress,

        # Rule Index, set to "append", "prepend" or any number
        [Parameter(
            Mandatory = $false
        )]
        [string]
        $RuleIndex

        # missing parameters by now: icmp_typename, src_mac_address, dst_mac_address, setting_preference, protocol_match_excepted
    )

    process {
        try {
            $fwRule = Get-UnifiFirewallRule -SiteName $SiteName | Where-Object { $_.RuleID -eq $RuleID }

            if ($fwRule) {

                $Body = @{}
                # Only add parameters which were given to the Rest-Body
                if ($PSBoundParameters.ContainsKey('RuleName')) {
                    $Body.name = $RuleName
                }

                if ($PSBoundParameters.ContainsKey('RuleSet')) {
                    $Body.ruleset = $RuleSet
                }

                if ($PSBoundParameters.ContainsKey('Action')) {
                    $Body.action = $Action.ToLower()
                }

                if ($PSBoundParameters.ContainsKey('Enabled')) {
                    $Body.enabled = $Enabled
                }

                if ($PSBoundParameters.ContainsKey('Protocol')) {
                    $Body.protocol = $Protocol.ToLower()
                }

                if ($PSBoundParameters.ContainsKey('Logging')) {
                    $Body.logging = $Logging.IsPresent
                }

                if ($PSBoundParameters.ContainsKey('StateNew')) {
                    $Body.state_new = $StateNew.IsPresent
                }

                if ($PSBoundParameters.ContainsKey('StateEstablished')) {
                    $Body.state_established = $StateEstablished.IsPresent
                }

                if ($PSBoundParameters.ContainsKey('StateInvalid')) {
                    $Body.state_invalid = $StateInvalid.IsPresent
                }
                
                if ($PSBoundParameters.ContainsKey('StateRelated')) {
                    $Body.state_related = $StateRelated.IsPresent
                }

                if ($PSBoundParameters.ContainsKey('IPSEC')) {
                    $Body.ipsec = $IPSEC
                }

                if ($PSBoundParameters.ContainsKey('SourceType')) {
                    $Body.src_networkconf_type = $SourceType
                }

                if ($PSBoundParameters.ContainsKey('SourceFirewallGroupIDs')) {
                    $Body.src_firewallgroup_ids = $SourceFirewallGroupIDs
                }

                if ($PSBoundParameters.ContainsKey('SourceNetworkID')) {
                    $Body.src_networkconf_id = $SourceNetworkID
                }

                if ($PSBoundParameters.ContainsKey('SourceAddress')) {
                    $Body.src_address = $SourceAddress
                }

                if ($PSBoundParameters.ContainsKey('DestinationType')) {
                    $Body.dst_networkconf_type = $DestinationType
                }

                if ($PSBoundParameters.ContainsKey('DestinationFirewallGroupIDs')) {
                    $Body.dst_firewallgroup_ids = $DestinationFirewallGroupIDs
                }

                if ($PSBoundParameters.ContainsKey('DestinationNetworkID')) {
                    $Body.dst_networkconf_id = $DestinationNetworkID
                }

                if ($PSBoundParameters.ContainsKey('DestinationAddress')) {
                    $Body.dst_address = $DestinationAddress
                }

                if ($PSBoundParameters.ContainsKey('RuleIndex')) {
                    $Body.rule_index = $RuleIndex
                }

                $Body = $Body | ConvertTo-Json
                
                $jsonResult = Invoke-UnifiRestCall -Method PUT -Route "api/s/$($SiteName)/rest/firewallrule/$($fwRule.RuleID)" -Body $Body

                if ($jsonResult.meta.rc -eq "ok") {
                    Write-Verbose "Firewall rule '$($fwRule.RuleName)' successfully edited for site '$SiteName'"
                    
                    if ($Raw) {
                        $jsonResult.data
                    } else {
                        $jsonResult.data | Select-Object    @{N="SiteName";E={$SiteName}},
                                                        @{N="SiteID";E={$_.site_id}},
                                                        @{N="RuleName";E={$_.Name}},
                                                        @{N="RuleID";E={$_._id}},
                                                        @{N="RuleSet";E={$_.ruleset}},
                                                        @{N="Enabled";E={$_.enabled}},
                                                        @{N="Action";E={$_.action}},
                                                        @{N="DstAddress";E={$_.dst_address}},
                                                        @{N="DstFirewallGroupIDs";E={$_.dst_firewallgroup_ids}},
                                                        @{N="DstNetworkConfID";E={$_.dst_networkconf_id}},
                                                        @{N="DstNetworkConfType";E={$_.dst_networkconf_type}},
                                                        @{N="IcmpTypename";E={$_.icmp_typename}},
                                                        @{N="IPSEC";E={$_.ipsec}},
                                                        @{N="Logging";E={$_.logging}},
                                                        @{N="Protocol";E={$_.protocol}},
                                                        @{N="ProtocolMatchExcepted";E={$_.protocol_match_excepted}},
                                                        @{N="RuleIndex";E={$_.rule_index}},
                                                        @{N="SrcAddress";E={$_.src_address}},
                                                        @{N="SrcFirewallGroupIDs";E={$_.src_firewallgroup_ids}},
                                                        @{N="SrcMACAddress";E={$_.src_mac_address}},
                                                        @{N="SrcNetworkConfID";E={$_.src_networkconf_id}},
                                                        @{N="SrcNetworkConfType";E={$_.src_networkconf_type}},
                                                        @{N="StateEstablished";E={$_.state_established}},
                                                        @{N="StateInvalid";E={$_.state_invalid}},
                                                        @{N="StateNew";E={$_.state_new}},
                                                        @{N="StateRelated";E={$_.state_related}},
                                                        @{N="SettingPreference";E={$_.setting_preference}}
                    }
                } else {
                    Write-Error "Firewall rule '$($fwRule.RuleName)' was NOT edited for site '$SiteName' -> error: $($jsonResult.meta.msg)"
                }
            } else {
                Write-Error "No Firewall rule with ID '$($fwRule.RuleID)' in site '$SiteName' was found"
            }

        } catch {
            Write-Warning "Something went wrong while editing firewall rule with ID '$($fwRule.RuleID)' for site '$SiteName' ($_)"
        }
    }
}

function Remove-UnifiFirewallRule {
    <#
    .SYNOPSIS
        Deletes a firewall rule in a site
    .DESCRIPTION
        Deletes a firewall rule in a site and asks for confirmation
    .EXAMPLE
        PS C:\> Get-UnifiSite "test" | Get-UnifiFirewallRule -RuleName "Allow RDP" | Remove-UnifiFirewallRule -Force
        Removes the firewall rule "Allow RDP" from the site "test" and does not ask for confirmation
    .OUTPUTS
        Returns $True on Success
        Returns $False on Failure
    #>

    [CmdletBinding()]
    [OutputType([Boolean])]

    param(
        # Name of the site (Unifi's internal name is used, not the name visible in the web interface)
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true )]
        [String]
        $SiteName,

        # ID of the Firewall group to be deleted
        [Parameter(
            Mandatory = $true,
            ValueFromPipelineByPropertyName = $true
        )]
        [ValidateNotNullOrEmpty()]
        [string]
        $RuleID,

        # Do not ask for confirmation
        [Parameter(Mandatory = $false)]
        [switch]
        $Force
    )
   
    process {
        try {
            $fwRule = Get-UnifiFirewallRule -SiteName $SiteName | Where-Object { $_.RuleID -eq $RuleID }

            if ($fwRule) {
                if (!$Force) {
                    do {
                        $answer = Read-Host -Prompt "Do you really want to delete the firewall rule '$($fwRule.RuleName)' (ID: $($RuleID))? (y/N): "
                    } while($answer -ne "y" -and $answer -ne "n" -and $answer -ne "")

                    if ($answer -eq "" -or $answer -eq "n") {
                        Write-Verbose "Deletion of firewall rule '$($fwRule.RuleName)' (ID: $($RuleID)) was aborted by user"
                        return $False
                    }

                }
                $jsonResult = Invoke-UnifiRestCall -Method DELETE -Route "api/s/$($SiteName)/rest/firewallrule/$($RuleID)"

                if ($jsonResult.meta.rc -eq "ok") {
                    Write-Verbose "Firewall rule '$($fwRule.RuleName)' successfully deleted for site $SiteName"
                    return $True
                } else {
                    Write-Error "Firewall rule '$($fwRule.RuleName)' was NOT deleted for site $SiteName"
                    return $False
                }
            } else {
                Write-Error "No Firewall rule with ID '$RuleID' was found in site '$SiteName'"
                return $False
            }

        } catch {
            Write-Warning "Something went wrong while removing firewall rule with ID '$($RuleID)' for site '$($SiteName)' ($_)"
        }
    }
}
#endregion

#region Tag Management
function Get-UnifiTag {
    <#
    .SYNOPSIS
        Gets all tags from a unifi site
    .DESCRIPTION
        Gets all tags from a unifi site
    .EXAMPLE
        PS C:\> Get-UnifiTag -SiteName "default"
        Get all tags from the "default" site
    .OUTPUTS
        Returns JSON-Data
    #>

    [CmdletBinding()]
    [OutputType([Object])]

    param(
        # Name of the site (Unifi's internal name is used, not the name visible in the web interface)
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true )]
        [String]
        $SiteName,

        # Do not filter or rename output, just sent the json result back as raw data
        [Parameter(Mandatory = $false)]
        [switch]
        $Raw
    )
   
    process {
        try {

            $jsonResult = Invoke-UnifiRestCall -Method GET -Route "api/s/$($SiteName)/rest/tag"

            if ($jsonResult.meta.rc -eq "ok") {
                if ($Raw) {
                    $jsonResult.data
                } else {
                    $jsonResult.data | Select-Object    @{N="SiteName";E={$SiteName}},
                                                        @{N="SiteID";E={$_.site_id}},
                                                        @{N="TagID";E={$_._id}},
                                                        @{N="TagName";E={$_.name}},
                                                        @{N="TagMembers";E={$_.member_table}}
                }
            }

        } catch {
            Write-Warning "Something went wrong while fetching tags for site '$($SiteName)' ($_)"
        }
    }
}

function New-UnifiTag {
    <#
    .SYNOPSIS
        Creates a new tag for a site
    .DESCRIPTION
        Creates a new tag for a site
    .EXAMPLE
        PS C:\> New-UnifiTag -SiteName "default" -TagName "Building-A" -TagMembers "00:11:22:33:44:55","66:77:88:99:AA:BB:CC"
        Creates the new Tag "Building-A" in the "default" site and assigns the Devices with the macs "00:11:22:33:44:55","66:77:88:99:AA:BB:CC" to it
    .OUTPUTS
        Returns JSON data
    #>

    [CmdletBinding()]
    [OutputType([Object])]

    param(
        # Name of the site (Unifi's internal name is used, not the name visible in the web interface)
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true )]
        [String]
        $SiteName,

        # Do not filter or rename output, just sent the json result back as raw data
        [Parameter(Mandatory = $false)]
        [switch]
        $Raw,

        # Name of the tag to be created
        [Parameter(
            Mandatory = $true
        )]
        [ValidateNotNullOrEmpty()]
        [string]
        $TagName,

        # Tag members (MAC-Addresses of APs). Can also be empty
        [Parameter(
            Mandatory = $false
        )]
        [string[]]
        $TagMembers = @()
    )
   
    process {
        try {

            $Body = @{
                name = $TagName
                member_table = $TagMembers
            } | ConvertTo-Json

            $jsonResult = Invoke-UnifiRestCall -Method POST -Route "api/s/$($SiteName)/rest/tag" -Body $Body

            if ($jsonResult.meta.rc -eq "ok") {
                Write-Verbose "Tag '$TagName' successfully created for site '$SiteName'"

                if ($Raw) {
                    $jsonResult.data
                } else {
                    $jsonResult.data | Select-Object    @{N="SiteName";E={$SiteName}},
                                                        @{N="SiteID";E={$_.site_id}},
                                                        @{N="TagID";E={$_._id}},
                                                        @{N="TagName";E={$_.name}},
                                                        @{N="TagMembers";E={$_.member_table}}
                }
            } else {
                Write-Error "Tag '$TagName' was NOT created for site '$SiteName')'"
            }

        } catch {
            Write-Warning "Something went wrong while creating a new tag for site '$($SiteName)' ($_)"
        }        
    }
}

function Edit-UnifiTag {
    <#
    .SYNOPSIS
        Edits a tag in a site
    .DESCRIPTION
        Edits a tag in a site
        You can control what should happen with $TagMembers by specifying the $Mode-Parameter
        $Mode = "Add" -> Add given Members to current TagMembers. This is the default
        $Mode = "Replace" -> Replace given members with current TagMembers
        $Mode = "Remove" -> Remove given members from current TagMembers (TODO: not implemented yet)
    .EXAMPLE
        PS C:\> Get-UnifiTag -SiteName "default" | Where-Object { $_.TagName -eq "Building-A" } | Edit-UnifiTag -GroupMembers "00:11:22:33:44:55","66:77:88:99:AA:BB:CC" -Mode Replace
        Edits the tag "Building-A" in the "default" site and replaces the members with "00:11:22:33:44:55","66:77:88:99:AA:BB:CC"
    .OUTPUTS
        Returns JSON-data
    #>

    [CmdletBinding()]
    [OutputType([Object])]

    param(
        # Name of the site (Unifi's internal name is used, not the name visible in the web interface)
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true )]
        [String]
        $SiteName,

        # Do not filter or rename output, just sent the json result back as raw data
        [Parameter(Mandatory = $false)]
        [switch]
        $Raw,

        # ID of the tag to edit
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true )]
        [ValidateNotNullOrEmpty()]
        [string]
        $TagID,

        # Name of the tag to to edit
        [Parameter(
            Mandatory = $false
        )]
        [ValidateNotNullOrEmpty()]
        [string]
        $TagName,

        # Tag members (MAC-Addresses of APs). Can also be empty
        [Parameter(
            Mandatory = $false
        )]
        [string[]]
        $TagMembers = @(),

        # Mode for updating the members
        [Parameter(
            Mandatory = $false
        )]
        [ValidateSet("Replace","Add","Remove")]
        [string]
        $Mode = "Add"
    )
   
    process {
        try {
            $Tag = Get-UnifiTag -SiteName $SiteName | Where-Object { $_.TagID -eq $TagID }

            if ($Tag) {

                # Use current name if no new name was given
                if ([String]::IsNullOrWhiteSpace($TagName)) {
                    $TagName = $Tag.TagName
                }

                # Use current members if no new members were given
                if ([String]::IsNullOrWhiteSpace($TagMembers)) {
                    $TagMembers = $Tag.TagMembers
                } else {
                    switch ($Mode) { # depending on the mode decide how to update the member table if $TagMembers has content
                        "Replace" {
                            $TagMembers = $TagMembers # nonsense, but it helps to understand the process
                        }

                        "Add" {
                            $TagMembers += $Tag.TagMembers
                        }

                        "Remove" {
                            Write-Warning "Remove-Mode is not fully implemented yet"
                            # TODO: $TagMembers += $Tag.TagMembers | Where-Object { $_ -ne $TagMembers }
                            $TagMembers = $Tag.TagMembers
                        }
                    }
                }

                $Body = @{
                    '_id' = $Tag.TagName
                    'site_id' = $Tag.TagID
                    name = $TagName
                    member_table = $TagMembers
                } | ConvertTo-Json
                
                $jsonResult = Invoke-UnifiRestCall -Method PUT -Route "api/s/$($siteName)/rest/tag/$($Tag.TagID)" -Body $Body

                if ($jsonResult.meta.rc -eq "ok") {
                    Write-Verbose "Tag '$TagName' successfully edited for site $SiteName"
                    
                    if ($Raw) {
                        $jsonResult.data
                    } else {
                        $jsonResult.data | Select-Object    @{N="SiteName";E={$SiteName}},
                                                            @{N="SiteID";E={$_.site_id}},
                                                            @{N="TagID";E={$_._id}},
                                                            @{N="TagName";E={$_.name}},
                                                            @{N="TagMembers";E={$_.member_table}}
                    }
                } else {
                    Write-Error "Tag '$TagName' was NOT edited for site $SiteName -> error: $($jsonResult.meta.msg)"
                }
            } else {
                Write-Error "No tag with ID '$TagID' in site '$SiteName' was found"
            }

        } catch {
            Write-Warning "Something went wrong while editing a tag for site '$SiteName' ($_)"
        }
        
    }
}

function Remove-UnifiTag {
    <#
    .SYNOPSIS
        Removes a tag from a site
    .DESCRIPTION
        Removes a tag from a site and asks for confirmation
    .EXAMPLE
        PS C:\> Get-UnifiTag -SiteName "default" | Where-Object { $_.TagName -eq "Building-A" } | Remove-UnifiTag -Force
        Removes the tag "Building-A" from the "default" site and skips confirmation
    .OUTPUTS
        Returns $True on Success
        Returns $False on Failure
    #>

    [CmdletBinding()]
    [OutputType([Boolean])]

    param(
        # Name of the site (Unifi's internal name is used, not the name visible in the web interface)
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true )]
        [String]
        $SiteName,

        # ID of the Firewall group to be deleted
        [Parameter(
            Mandatory = $true,
            ValueFromPipelineByPropertyName = $true
        )]
        [ValidateNotNullOrEmpty()]
        [string]
        $TagID,

        # Do not ask for confirmation
        [Parameter(Mandatory = $false)]
        [switch]
        $Force
    )
   
    process {
        try {
            $Tag = Get-UnifiTag -SiteName $SiteName | Where-Object { $_.TagID -eq $TagID }

            if ($Tag) {
                if (!$Force) {
                    do {
                        $answer = Read-Host -Prompt "Do you really want to delete the tag '$($Tag.TagName)' (ID: $($TagID))? (y/N): "
                    } while($answer -ne "y" -and $answer -ne "n" -and $answer -ne "")

                    if ($answer -eq "" -or $answer -eq "n") {
                        Write-Verbose "Deletion of tag '$($Tag.TagName)' (ID: $($TagID)) was aborted by user"
                        return $False
                    }

                }
                $jsonResult = Invoke-UnifiRestCall -Method DELETE -Route "api/s/$($SiteName)/rest/tag/$($TagID)"

                if ($jsonResult.meta.rc -eq "ok") {
                    Write-Verbose "Tag '$($Tag.TagName)' successfully deleted for site $SiteName"
                    return $True
                } else {
                    Write-Error "Tag '$($Tag.TagName)' was NOT deleted for site $SiteName"
                    return $False
                }
            } else {
                Write-Error "No Tag with ID '$TagID' was found in site $SiteName"
                return $False
            }

        } catch {
            Write-Warning "Something went wrong while deleting a a tag from site $($SiteName) ($_)"
            return $False
        }
    }
}
#endregion