unifiPS.psm1

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

function Invoke-UnifiRestCall {
    [CmdletBinding()]
    [OutputType([Object])]

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

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

        # Rest Body
        [Parameter(Mandatory = $False)]
        [Object]
        $Body,

        # Rest Body
        [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 ($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
            }
        }

    }
}

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://192.168.178.1:8443/api -Timeout 5
        Logs in to the unifi server at the specified address and wait max. 5 seconds
    #>

    [CmdletBinding()]

    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

        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;
    }
}
"@

        } catch {}

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

        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"
        }

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

        $Credential = $null
        $Body = $null

        if ($jsonResult.meta.rc -eq "ok") {
            Write-Host -ForegroundColor Green "Login to Unifi-Controller successful"
        } else {
            Write-Host -ForegroundColor Red "Login to Unifi-Controller failed"
        }
    }
}

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
    #>

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

    param()

    begin {
    }

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

        if ($jsonResult.meta.rc -eq "ok") {
            Write-Host -ForegroundColor Green "Logout from Unifi-Controller successful"
        } else {
            Write-Host -ForegroundColor Red "Logout from Unifi-Controller failed"
        }
        
    }
}

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)
    .EXAMPLE
        PS C:\> Get-UnifiServerInfo
        Grabs the information
    #>

    [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}}
            }
        }
    }
}

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
    #>

    [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-UnifiSite {
    <#
    .SYNOPSIS
        Gets all sites of the unifi controller
    .DESCRIPTION
        Gets all sites of the unifi controller
        You can filter by name (internal site name) or id or DisplayName (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"
    #>

    [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 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).
        Each of this entries are returned as a single hashtable
 
        You can pipe the output from "Get-UnifiSite" to this cmdlet
    .EXAMPLE
        PS C:\> Get-UnifiSiteInfo
        Gets information from all sites
    .EXAMPLE
        PS C:\> Get-UnifiSiteInfo -friendlyName "*Einhard*"
        Gets information from all sites which contains the string "Einhard"
    #>

    [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
        }
        
    }
}

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"
    #>

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

    param(
        # 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,

        # SiteName
        [Parameter(Mandatory = $false, ParameterSetName="SiteName", ValueFromPipelineByPropertyName=$True)]
        [string]
        $SiteName
    )
   
    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}}
                    }

                }
            }
        }
    }
}

function Get-UnifiEvent {
    <#
    .SYNOPSIS
        Gets events for a unifi site
    .DESCRIPTION
        Gets events for a unifi site
 
        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"
        Gets events from the site with the (internal) name "01gg6pt0". If you want to use the display name for searching see the previous example
    #>

    [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
 
        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". If you want to use the display name for searching see the previous example
    #>

    [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
        }
        
    }
}

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"
    #>

    [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,
                                                        Name,
                                                        Serial,
                                                        Version,
                                                        @{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 sites ($($_.Exception))" -ErrorAction Stop
        }
    }
}

function Restart-UnifiDevice {
    <#
    .SYNOPSIS
        Restarts a unifi device
    .DESCRIPTION
        Restarts a unifi device
 
        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"
    .EXAMPLE
        PS C:\> Restart-UnifiDevice -SiteName "Test" -MAC "00:11:22:33:44:55"
        Restarts the device with the mac "00:11:22:33:44:55" in site "Test"
    #>

    [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,

        # MAC of the device 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 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-Host -ForegroundColor Yellow "Device with '$MAC' will reboot now"
            }

        } catch {
            Write-Error "Something went wrong while fetching sites ($($_.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
        Restarts the device with the name "AP01" in site "Test"
    .EXAMPLE
        PS C:\> Sync-UnifiDevice -SiteName "Test" -MAC "00:11:22:33:44:55"
        Restarts the device with the mac "00:11:22:33:44:55" in site "Test"
    #>

    [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,

        # 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-Host -ForegroundColor Yellow "Device with '$MAC' will force a provision"
            }

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

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
    .EXAMPLE
        PS C:\> Get-UnifiClient -SiteName "default"
        Returns all clients from site "default"
    #>

    [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 sites ($($_.Exception))" -ErrorAction Stop
        }
    }
}

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)
 
        You can pipe the output from "Get-UnifiClient" to this cmdlet
    .EXAMPLE
        PS C:\> Get-UnifiSite "Test" | Get-UnifiDevice "iPad01" | Disconnect-UnifiClient
        Restarts the client with the name "iPad01" in site "Test"
    .EXAMPLE
        PS C:\> Disconnect-UnifiClient -SiteName "Test" -MAC "00:11:22:33:44:55"
        Restarts the client with the mac "00:11:22:33:44:55" in site "Test"
    #>

    [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,

        # 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-Host -ForegroundColor Yellow "Client '$MAC' was kicked"
            }

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

function Get-UnifiFirewallGroup {
    <#
    .SYNOPSIS
        Lists firewall groups in a site
    .DESCRIPTION
        TODO
    .EXAMPLE
        PS C:\> Get-UnifiFirewallGroup 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
    )
   
    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 creating a new firewall group for site $($site.friendlyName) ($_)"
        }
        
    }
}

function New-UnifiFirewallGroup {
    <#
    .SYNOPSIS
        Creates a new firewall group in a site
    .DESCRIPTION
        TODO
    .EXAMPLE
        PS C:\> New-UnifiFirewallGroup 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,

        # 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 $($site.friendlyName) ($_)"
        }
        
    }
}

function Edit-UnifiFirewallGroup {
    <#
    .SYNOPSIS
        Edits a firewall group in a site
    .DESCRIPTION
        TODO
    .EXAMPLE
        PS C:\> Edit-UnifiFirewallGroup 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]
        $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. Will be overridden
        [Parameter(
            Mandatory = $false
        )]
        [string[]]
        $GroupMembers = @()
    )
   
    process {
        
        try {
            $fwGroup = Get-UnifiFirewallGroup -SiteName $SiteName | Where-Object { $_.GroupID -eq $GroupID }

            if ($fwGroup) {

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

                $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 creating a new firewall group for site $SiteName ($_)"
        }
        
    }
}

function Remove-UnifiFirewallGroup {
    <#
    .SYNOPSIS
        Deletes a firewall group in a site
    .DESCRIPTION
        TODO
    .EXAMPLE
        PS C:\> Remove-UnifiFirewallGroup 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,

        # 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 $null
                    }

                }
                $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"
                } else {
                    Write-Error "Firewall group '$($fwGroup.GroupName)' was NOT deleted for site $SiteName"
                }
            } else {
                Write-Error "No Firewall Group with $GroupID was found in site $SiteName"
            }

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

function Get-UnifiFirewallRule {
    <#
    .SYNOPSIS
        Lists firewall rules in a site
    .DESCRIPTION
        TODO
    .EXAMPLE
        PS C:\> Get-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
    )
   
    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 creating a new firewall group for site $($SiteName) ($_)"
        }
        
    }
}

function New-UnifiFirewallRule {
    <#
    .SYNOPSIS
        Creates a new firewall group in a site
    .DESCRIPTION
        TODO
    .EXAMPLE
        PS C:\> New-UnifiFirewallGroup 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,

        # 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 o 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 {
                if ($jsonResult.meta.msg -eq "api.err.FirewallGroupExisted") {
                    Write-Warning "Firewall rule '$RuleName' already exists in site $SiteName"
                } else {
                    Write-Error "Firewall rule '$RuleName' was NOT created for site $SiteName"
                }
            }

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

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 = "all",

        # Should be logged o 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 {
        Write-Warning "This cmdlet does not work at the moment due to 'invalidpayload' errors"
        try {
            $fwRule = Get-UnifiFirewallRule -SiteName $SiteName | Where-Object { $_.RuleID -eq $RuleID }

            if ($fwRule) {

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

                # Use current destination group IDs if no ones were given
                if ([String]::IsNullOrWhiteSpace($DestinationFirewallGroupIDs)) {
                    $DestinationFirewallGroupIDs = $fwRule.DstFirewallGroupIDs
                }

                $Body = @{
                    "_id"                   = $fwRule.RuleID
                    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                 = $fwRule.SiteID
                    setting_preference      = "manual"
                } | 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 group $GroupName successfully edited for site $SiteName"
                    
                    if ($Raw) {
                        $jsonResult.data
                    } else {
                        $jsonResult.data # TODO
                    }
                } 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 creating a new firewall group for site $SiteName ($_)"
        }
        
    }
}

function Remove-UnifiFirewallRule {
    <#
    .SYNOPSIS
        Deletes a firewall rule in a site
    .DESCRIPTION
        TODO
    .EXAMPLE
        PS C:\> Remove-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,

        # 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 $null
                    }

                }
                $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"
                } else {
                    Write-Error "Firewall rule '$($fwRule.RuleName)' was NOT deleted for site $SiteName"
                }
            } else {
                Write-Error "No Firewall rule with $RuleID was found in site $SiteName"
            }

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

function Get-UnifiTag {
    <#
    .SYNOPSIS
        Gets all tags from a unifi site
    .DESCRIPTION
        TODO
    .EXAMPLE
        PS C:\> Get-UnifiTag 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
    )
   
    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 firewall group in a site
    .DESCRIPTION
        TODO
    .EXAMPLE
        PS C:\> New-UnifiFirewallGroup 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,

        # 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
        TODO
    .EXAMPLE
        PS C:\> Edit-UnifiTag 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 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 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
        TODO
    .EXAMPLE
        PS C:\> Remove-UnifiTag 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,

        # 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 $null
                    }

                }
                $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"
                } else {
                    Write-Error "Tag '$($Tag.TagName)' was NOT deleted for site $SiteName"
                }
            } else {
                Write-Error "No Tag with ID '$TagID' was found in site $SiteName"
            }

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