diff --git a/PiHoleShell/PiHoleShell.psm1 b/PiHoleShell/PiHoleShell.psm1
index 59967d5..da20726 100644
--- a/PiHoleShell/PiHoleShell.psm1
+++ b/PiHoleShell/PiHoleShell.psm1
@@ -40,5 +40,9 @@ Export-ModuleMember -Function @(
#Teleporter
'Get-PiHoleTeleporterDownload', `
#DomainManagement
- 'Get-PiHoleDomain', 'New-PiHoleDomain', 'Update-PiHoleDomain', 'Remove-PiHoleDomain'
+ 'Get-PiHoleDomain', 'New-PiHoleDomain', 'Update-PiHoleDomain', 'Remove-PiHoleDomain', `
+ #ClientManagement
+ 'Get-PiHoleClient', 'New-PiHoleClient', 'Update-PiHoleClient', 'Remove-PiHoleClient', 'Get-PiHoleClientSuggestion', `
+ #NetworkInformation
+ 'Get-PiHoleNetworkGateway', 'Get-PiHoleNetworkRoute', 'Get-PiHoleNetworkInterface', 'Get-PiHoleNetworkDevice', 'Remove-PiHoleNetworkDevice'
)
\ No newline at end of file
diff --git a/PiHoleShell/Public/ClientManagement/Get-PiHoleClient.ps1 b/PiHoleShell/Public/ClientManagement/Get-PiHoleClient.ps1
new file mode 100644
index 0000000..799ff99
--- /dev/null
+++ b/PiHoleShell/Public/ClientManagement/Get-PiHoleClient.ps1
@@ -0,0 +1,97 @@
+function Get-PiHoleClient {
+ <#
+.SYNOPSIS
+Get clients
+
+.DESCRIPTION
+Request Pi-hole's configured clients (used to apply group-based rules to specific devices).
+Omit -Client to get every configured client; specify it to get just that one.
+
+.PARAMETER PiHoleServer
+The URL to the PiHole Server, for example "http://pihole.domain.com:8080", or "http://192.168.1.100"
+
+.PARAMETER Password
+The API Password you generated from your PiHole server
+
+.PARAMETER Client
+A specific client to return - an IP address, IP subnet (CIDR), MAC address, hostname, or
+interface (prefixed with a colon, e.g. ":eth0"). Omit to return every configured client
+
+.PARAMETER IgnoreSsl
+Set to $true to skip SSL certificate validation
+
+.PARAMETER RawOutput
+This will dump the response instead of the formatted object
+
+.EXAMPLE
+Get-PiHoleClient -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password"
+
+.EXAMPLE
+Get-PiHoleClient -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" -Client "192.168.1.50"
+ #>
+ [CmdletBinding(HelpUri = 'https://ftl.pi-hole.net/master/docs/#get-/clients/-client-')]
+ [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")]
+ param (
+ [Parameter(Mandatory = $true)]
+ [System.URI]$PiHoleServer,
+ [Parameter(Mandatory = $true)]
+ [string]$Password,
+ [string]$Client,
+ [bool]$IgnoreSsl = $false,
+ [bool]$RawOutput = $false
+ )
+ try {
+ $Sid = Request-PiHoleAuth -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl
+
+ $Groups = Get-PiHoleGroup -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl
+
+ $Uri = "$($PiHoleServer.OriginalString)/api/clients"
+ if ($Client) {
+ $Uri += "/$([System.Uri]::EscapeDataString($Client))"
+ }
+
+ $Params = @{
+ Headers = @{sid = $($Sid) }
+ Uri = $Uri
+ Method = "Get"
+ SkipCertificateCheck = $IgnoreSsl
+ ContentType = "application/json"
+ }
+
+ $Response = Invoke-RestMethod @Params
+
+ if ($RawOutput) {
+ Write-Output $Response
+ }
+
+ else {
+ $ObjectFinal = foreach ($Item in $Response.clients) {
+ $GroupNames = [System.Collections.ArrayList]@()
+ foreach ($Group in $Item.groups) {
+ $GroupNames += ($Groups | Where-Object { $_.Id -eq $Group }).Name
+ }
+
+ [PSCustomObject]@{
+ Client = $Item.client
+ Name = $Item.name
+ Comment = $Item.comment
+ Groups = $GroupNames
+ Id = $Item.id
+ DateAdded = (Convert-PiHoleUnixTimeToLocalTime -UnixTime $Item.date_added).LocalTime
+ DateModified = (Convert-PiHoleUnixTimeToLocalTime -UnixTime $Item.date_modified).LocalTime
+ }
+ }
+ Write-Output $ObjectFinal
+ }
+ }
+
+ catch {
+ Write-Error -Message $_.Exception.Message
+ }
+
+ finally {
+ if ($Sid) {
+ Remove-PiHoleCurrentAuthSession -PiHoleServer $PiHoleServer -Sid $Sid -IgnoreSsl $IgnoreSsl
+ }
+ }
+}
diff --git a/PiHoleShell/Public/ClientManagement/Get-PiHoleClientSuggestion.ps1 b/PiHoleShell/Public/ClientManagement/Get-PiHoleClientSuggestion.ps1
new file mode 100644
index 0000000..e1ca8fc
--- /dev/null
+++ b/PiHoleShell/Public/ClientManagement/Get-PiHoleClientSuggestion.ps1
@@ -0,0 +1,75 @@
+function Get-PiHoleClientSuggestion {
+ <#
+.SYNOPSIS
+Get client suggestions
+
+.DESCRIPTION
+Returns a list of clients Pi-hole has seen making DNS queries but that aren't yet configured as
+a client (see New-PiHoleClient to add one).
+
+.PARAMETER PiHoleServer
+The URL to the PiHole Server, for example "http://pihole.domain.com:8080", or "http://192.168.1.100"
+
+.PARAMETER Password
+The API Password you generated from your PiHole server
+
+.PARAMETER IgnoreSsl
+Set to $true to skip SSL certificate validation
+
+.PARAMETER RawOutput
+This will dump the response instead of the formatted object
+
+.EXAMPLE
+Get-PiHoleClientSuggestion -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password"
+ #>
+ [CmdletBinding(HelpUri = 'https://ftl.pi-hole.net/master/docs/#get-/clients/_suggestions')]
+ [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")]
+ param (
+ [Parameter(Mandatory = $true)]
+ [System.URI]$PiHoleServer,
+ [Parameter(Mandatory = $true)]
+ [string]$Password,
+ [bool]$IgnoreSsl = $false,
+ [bool]$RawOutput = $false
+ )
+ try {
+ $Sid = Request-PiHoleAuth -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl
+
+ $Params = @{
+ Headers = @{sid = $($Sid) }
+ Uri = "$($PiHoleServer.OriginalString)/api/clients/_suggestions"
+ Method = "Get"
+ SkipCertificateCheck = $IgnoreSsl
+ ContentType = "application/json"
+ }
+
+ $Response = Invoke-RestMethod @Params
+
+ if ($RawOutput) {
+ Write-Output $Response
+ }
+
+ else {
+ $ObjectFinal = foreach ($Item in $Response.clients) {
+ [PSCustomObject]@{
+ HwAddr = $Item.hwaddr
+ MacVendor = $Item.macVendor
+ LastQuery = (Convert-PiHoleUnixTimeToLocalTime -UnixTime $Item.lastQuery).LocalTime
+ Addresses = $Item.addresses
+ Names = $Item.names
+ }
+ }
+ Write-Output $ObjectFinal
+ }
+ }
+
+ catch {
+ Write-Error -Message $_.Exception.Message
+ }
+
+ finally {
+ if ($Sid) {
+ Remove-PiHoleCurrentAuthSession -PiHoleServer $PiHoleServer -Sid $Sid -IgnoreSsl $IgnoreSsl
+ }
+ }
+}
diff --git a/PiHoleShell/Public/ClientManagement/New-PiHoleClient.ps1 b/PiHoleShell/Public/ClientManagement/New-PiHoleClient.ps1
new file mode 100644
index 0000000..703d473
--- /dev/null
+++ b/PiHoleShell/Public/ClientManagement/New-PiHoleClient.ps1
@@ -0,0 +1,121 @@
+function New-PiHoleClient {
+ <#
+.SYNOPSIS
+Add a new client
+
+.DESCRIPTION
+Adds a client to Pi-hole so group-based rules can be applied to it specifically. A client may
+be identified by IP address, IP subnet (CIDR notation), MAC address, hostname, or the interface
+it connects through (prefixed with a colon, e.g. ":eth0"). IP-based recognition is preferred -
+MAC address, hostname, and interface recognition only work for devices Pi-hole has already seen.
+
+.PARAMETER PiHoleServer
+The URL to the PiHole Server, for example "http://pihole.domain.com:8080", or "http://192.168.1.100"
+
+.PARAMETER Password
+The API Password you generated from your PiHole server
+
+.PARAMETER Client
+The client to add - an IP address, IP subnet (CIDR), MAC address, hostname, or interface
+(prefixed with a colon, e.g. ":eth0")
+
+.PARAMETER Comment
+An optional comment to store alongside the client
+
+.PARAMETER Group
+The group(s) this client applies to. Defaults to "Default"
+
+.PARAMETER IgnoreSsl
+Set to $true to skip SSL certificate validation
+
+.PARAMETER RawOutput
+This will dump the response instead of the formatted object
+
+.EXAMPLE
+New-PiHoleClient -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" -Client "192.168.1.50" -Comment "Kid's tablet"
+ #>
+ [CmdletBinding(HelpUri = 'https://ftl.pi-hole.net/master/docs/#post-/clients')]
+ [Diagnostics.CodeAnalysis.SuppressMessage("PSUseShouldProcessForStateChangingFunctions", "", Justification = "Ignoring for now")]
+ [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")]
+ param (
+ [Parameter(Mandatory = $true)]
+ [System.URI]$PiHoleServer,
+ [Parameter(Mandatory = $true)]
+ [string]$Password,
+ [Parameter(Mandatory = $true)]
+ [string]$Client,
+ [string]$Comment = $null,
+ [string[]]$Group = "Default",
+ [bool]$IgnoreSsl = $false,
+ [bool]$RawOutput = $false
+ )
+ try {
+ $FindMatchingClient = Get-PiHoleClient -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl -Client $Client
+
+ if ($FindMatchingClient) {
+ throw "Client $Client already exists on $PiHoleServer! Please use Update-PiHoleClient to update it"
+ }
+
+ $AllGroups = Get-PiHoleGroup -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl
+ $AllGroupsNames = @()
+ $AllGroupsIds = @()
+ foreach ($GroupItem in $Group) {
+ $FoundGroup = $AllGroups | Where-Object { $_.Name -eq $GroupItem }
+ if ($FoundGroup) {
+ $AllGroupsNames += $FoundGroup.Name
+ $AllGroupsIds += $FoundGroup.Id
+ }
+ else {
+ throw "Cannot find $GroupItem on $PiHoleServer! Please use Get-PiHoleGroup to list all groups"
+ }
+ }
+
+ $Sid = Request-PiHoleAuth -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl
+
+ $Body = @{
+ client = $Client
+ comment = $Comment
+ groups = [Object[]]($AllGroupsIds)
+ }
+
+ $Params = @{
+ Headers = @{sid = $($Sid) }
+ Uri = "$($PiHoleServer.OriginalString)/api/clients"
+ Method = "Post"
+ SkipCertificateCheck = $IgnoreSsl
+ Body = $Body | ConvertTo-Json -Depth 10
+ ContentType = "application/json"
+ }
+
+ $Response = Invoke-RestMethod @Params
+
+ if ($RawOutput) {
+ Write-Output $Response
+ }
+
+ else {
+ $ObjectFinal = foreach ($Item in $Response.clients) {
+ [PSCustomObject]@{
+ Client = $Item.client
+ Name = $Item.name
+ Comment = $Item.comment
+ Groups = $AllGroupsNames
+ Id = $Item.id
+ DateAdded = (Convert-PiHoleUnixTimeToLocalTime -UnixTime $Item.date_added).LocalTime
+ DateModified = (Convert-PiHoleUnixTimeToLocalTime -UnixTime $Item.date_modified).LocalTime
+ }
+ }
+ Write-Output $ObjectFinal
+ }
+ }
+
+ catch {
+ Write-Error -Message $_.Exception.Message
+ }
+
+ finally {
+ if ($Sid) {
+ Remove-PiHoleCurrentAuthSession -PiHoleServer $PiHoleServer -Sid $Sid -IgnoreSsl $IgnoreSsl
+ }
+ }
+}
diff --git a/PiHoleShell/Public/ClientManagement/Remove-PiHoleClient.ps1 b/PiHoleShell/Public/ClientManagement/Remove-PiHoleClient.ps1
new file mode 100644
index 0000000..0855c67
--- /dev/null
+++ b/PiHoleShell/Public/ClientManagement/Remove-PiHoleClient.ps1
@@ -0,0 +1,90 @@
+function Remove-PiHoleClient {
+ <#
+.SYNOPSIS
+Remove a client
+
+.DESCRIPTION
+Removes a client from Pi-hole. The Pi-hole API deletes clients in a batch, so this sends a
+single-item batch containing just the client you specify.
+
+.PARAMETER PiHoleServer
+The URL to the PiHole Server, for example "http://pihole.domain.com:8080", or "http://192.168.1.100"
+
+.PARAMETER Password
+The API Password you generated from your PiHole server
+
+.PARAMETER Client
+The client to remove - an IP address, IP subnet (CIDR), MAC address, hostname, or interface
+(prefixed with a colon, e.g. ":eth0")
+
+.PARAMETER IgnoreSsl
+Set to $true to skip SSL certificate validation
+
+.PARAMETER RawOutput
+This will dump the response instead of the formatted object
+
+.EXAMPLE
+Remove-PiHoleClient -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" -Client "192.168.1.50"
+ #>
+ [CmdletBinding(SupportsShouldProcess = $true, HelpUri = 'https://ftl.pi-hole.net/master/docs/#post-/clients-batchDelete')]
+ [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")]
+ param (
+ [Parameter(Mandatory = $true)]
+ [System.URI]$PiHoleServer,
+ [Parameter(Mandatory = $true)]
+ [string]$Password,
+ [Parameter(Mandatory = $true)]
+ [string]$Client,
+ [bool]$IgnoreSsl = $false,
+ [bool]$RawOutput = $false
+ )
+ try {
+ $Target = "Pi-Hole client $Client"
+ if ($PSCmdlet.ShouldProcess($Target, "Remove client")) {
+ $Sid = Request-PiHoleAuth -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl
+
+ $Body = @(
+ @{
+ item = $Client
+ }
+ )
+
+ #For some reason this needs to be here to make it an array
+ $Body = , $Body
+ $Params = @{
+ Headers = @{sid = $($Sid) }
+ Uri = "$($PiHoleServer.OriginalString)/api/clients:batchDelete"
+ Method = "Post"
+ SkipCertificateCheck = $IgnoreSsl
+ Body = $Body | ConvertTo-Json -Depth 10 -Compress
+ ContentType = "application/json"
+ }
+
+ $Response = Invoke-RestMethod @Params
+
+ if ($RawOutput) {
+ Write-Output $Response
+ }
+
+ else {
+ # A successful delete returns 204 No Content, so there's no response body to
+ # build a rich object from.
+ $Object = [PSCustomObject]@{
+ Client = $Client
+ Status = "Removed"
+ }
+ Write-Output $Object
+ }
+ }
+ }
+
+ catch {
+ Write-Error -Message $_.Exception.Message
+ }
+
+ finally {
+ if ($Sid) {
+ Remove-PiHoleCurrentAuthSession -PiHoleServer $PiHoleServer -Sid $Sid -IgnoreSsl $IgnoreSsl
+ }
+ }
+}
diff --git a/PiHoleShell/Public/ClientManagement/Update-PiHoleClient.ps1 b/PiHoleShell/Public/ClientManagement/Update-PiHoleClient.ps1
new file mode 100644
index 0000000..f7319af
--- /dev/null
+++ b/PiHoleShell/Public/ClientManagement/Update-PiHoleClient.ps1
@@ -0,0 +1,129 @@
+function Update-PiHoleClient {
+ <#
+.SYNOPSIS
+Update a client
+
+.DESCRIPTION
+Updates an existing client's Comment and/or Group(s). The underlying Pi-hole API replaces the
+entire client entry on update, so any property you don't pass here is preserved by first
+reading the client's current value and resending it - nothing is silently cleared just because
+you only meant to change one property.
+
+.PARAMETER PiHoleServer
+The URL to the PiHole Server, for example "http://pihole.domain.com:8080", or "http://192.168.1.100"
+
+.PARAMETER Password
+The API Password you generated from your PiHole server
+
+.PARAMETER Client
+The client to update - an IP address, IP subnet (CIDR), MAC address, hostname, or interface
+(prefixed with a colon, e.g. ":eth0")
+
+.PARAMETER Comment
+The new comment for the client. Leave unset to keep the client's current comment
+
+.PARAMETER Group
+The group(s) this client should apply to. Leave unset to keep the client's current group(s)
+
+.PARAMETER IgnoreSsl
+Set to $true to skip SSL certificate validation
+
+.PARAMETER RawOutput
+This will dump the response instead of the formatted object
+
+.EXAMPLE
+Update-PiHoleClient -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" -Client "192.168.1.50" -Comment "Kid's tablet"
+ #>
+ [CmdletBinding(HelpUri = 'https://ftl.pi-hole.net/master/docs/#put-/clients/-client-')]
+ [Diagnostics.CodeAnalysis.SuppressMessage("PSUseShouldProcessForStateChangingFunctions", "", Justification = "Ignoring for now")]
+ [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")]
+ param (
+ [Parameter(Mandatory = $true)]
+ [System.URI]$PiHoleServer,
+ [Parameter(Mandatory = $true)]
+ [string]$Password,
+ [Parameter(Mandatory = $true)]
+ [string]$Client,
+ [string]$Comment,
+ [string[]]$Group,
+ [bool]$IgnoreSsl = $false,
+ [bool]$RawOutput = $false
+ )
+
+ try {
+ if (-not $PSBoundParameters.ContainsKey('Comment') -and -not $PSBoundParameters.ContainsKey('Group')) {
+ throw "To update $Client, you must specify the Comment and/or Group parameter"
+ }
+
+ $ExistingClient = Get-PiHoleClient -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl -Client $Client
+
+ if (-not $ExistingClient) {
+ throw "Cannot find $Client on $PiHoleServer! Please use New-PiHoleClient to create it"
+ }
+
+ $AllGroups = Get-PiHoleGroup -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl
+
+ $GroupNamesToResolve = if ($PSBoundParameters.ContainsKey('Group')) { $Group } else { $ExistingClient.Groups }
+
+ $AllGroupsNames = @()
+ $AllGroupsIds = @()
+ foreach ($GroupItem in $GroupNamesToResolve) {
+ $FoundGroup = $AllGroups | Where-Object { $_.Name -eq $GroupItem }
+ if ($FoundGroup) {
+ $AllGroupsNames += $FoundGroup.Name
+ $AllGroupsIds += $FoundGroup.Id
+ }
+ else {
+ throw "Cannot find $GroupItem on $PiHoleServer! Please use Get-PiHoleGroup to list all groups"
+ }
+ }
+
+ $Sid = Request-PiHoleAuth -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl
+
+ # The API replaces the whole client entry on update, so any property not explicitly
+ # passed here is resent using the client's current value to avoid silently clearing it.
+ $Body = @{
+ comment = if ($PSBoundParameters.ContainsKey('Comment')) { $Comment } else { $ExistingClient.Comment }
+ groups = [Object[]]($AllGroupsIds)
+ }
+
+ $Params = @{
+ Headers = @{sid = $($Sid) }
+ Uri = "$($PiHoleServer.OriginalString)/api/clients/$([System.Uri]::EscapeDataString($Client))"
+ Method = "Put"
+ SkipCertificateCheck = $IgnoreSsl
+ Body = $Body | ConvertTo-Json -Depth 10
+ ContentType = "application/json"
+ }
+
+ $Response = Invoke-RestMethod @Params
+
+ if ($RawOutput) {
+ Write-Output $Response
+ }
+ else {
+ $ObjectFinal = foreach ($Item in $Response.clients) {
+ [PSCustomObject]@{
+ Client = $Item.client
+ Name = $Item.name
+ Comment = $Item.comment
+ Groups = $AllGroupsNames
+ Id = $Item.id
+ DateAdded = (Convert-PiHoleUnixTimeToLocalTime -UnixTime $Item.date_added).LocalTime
+ DateModified = (Convert-PiHoleUnixTimeToLocalTime -UnixTime $Item.date_modified).LocalTime
+ }
+ }
+ Write-Output $ObjectFinal
+ }
+ }
+
+ catch {
+ Write-Error -Message $_.Exception.Message
+ }
+
+ finally {
+ if ($Sid) {
+ Remove-PiHoleCurrentAuthSession -PiHoleServer $PiHoleServer -Sid $Sid -IgnoreSsl $IgnoreSsl
+ }
+ }
+}
diff --git a/PiHoleShell/Public/NetworkInformation/Get-PiHoleNetworkDevice.ps1 b/PiHoleShell/Public/NetworkInformation/Get-PiHoleNetworkDevice.ps1
new file mode 100644
index 0000000..f47050b
--- /dev/null
+++ b/PiHoleShell/Public/NetworkInformation/Get-PiHoleNetworkDevice.ps1
@@ -0,0 +1,110 @@
+function Get-PiHoleNetworkDevice {
+ <#
+.SYNOPSIS
+Get info about the devices in your local network as seen by your Pi-hole
+
+.DESCRIPTION
+Returns the devices Pi-hole has seen on your network, ordered by most recent query first. Shown
+devices default to 10; use -MaxDevices to change that.
+
+.PARAMETER PiHoleServer
+The URL to the PiHole Server, for example "http://pihole.domain.com:8080", or "http://192.168.1.100"
+
+.PARAMETER Password
+The API Password you generated from your PiHole server
+
+.PARAMETER MaxDevices
+Maximum number of devices to return. Defaults to 10
+
+.PARAMETER MaxAddresses
+Maximum number of addresses to return per device
+
+.PARAMETER IgnoreSsl
+Set to $true to skip SSL certificate validation
+
+.PARAMETER RawOutput
+This will dump the response instead of the formatted object
+
+.EXAMPLE
+Get-PiHoleNetworkDevice -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password"
+
+.EXAMPLE
+Get-PiHoleNetworkDevice -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" -MaxDevices 50
+ #>
+ [CmdletBinding(HelpUri = 'https://ftl.pi-hole.net/master/docs/#get-/network/devices')]
+ [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")]
+ param (
+ [Parameter(Mandatory = $true)]
+ [System.URI]$PiHoleServer,
+ [Parameter(Mandatory = $true)]
+ [string]$Password,
+ [Nullable[int]]$MaxDevices,
+ [Nullable[int]]$MaxAddresses,
+ [bool]$IgnoreSsl = $false,
+ [bool]$RawOutput = $false
+ )
+ try {
+ $Sid = Request-PiHoleAuth -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl
+
+ $QueryParams = [System.Collections.ArrayList]@()
+ if ($PSBoundParameters.ContainsKey('MaxDevices')) {
+ $QueryParams.Add("max_devices=$MaxDevices") | Out-Null
+ }
+ if ($PSBoundParameters.ContainsKey('MaxAddresses')) {
+ $QueryParams.Add("max_addresses=$MaxAddresses") | Out-Null
+ }
+
+ $Uri = "$($PiHoleServer.OriginalString)/api/network/devices"
+ if ($QueryParams.Count -gt 0) {
+ $Uri += "?" + ($QueryParams -join '&')
+ }
+
+ $Params = @{
+ Headers = @{sid = $($Sid) }
+ Uri = $Uri
+ Method = "Get"
+ SkipCertificateCheck = $IgnoreSsl
+ ContentType = "application/json"
+ }
+
+ $Response = Invoke-RestMethod @Params
+
+ if ($RawOutput) {
+ Write-Output $Response
+ }
+ else {
+ $ObjectFinal = foreach ($Item in $Response.devices) {
+ $Ips = foreach ($IpItem in $Item.ips) {
+ [PSCustomObject]@{
+ Ip = $IpItem.ip
+ Name = $IpItem.name
+ LastSeen = (Convert-PiHoleUnixTimeToLocalTime -UnixTime $IpItem.lastSeen).LocalTime
+ NameUpdated = (Convert-PiHoleUnixTimeToLocalTime -UnixTime $IpItem.nameUpdated).LocalTime
+ }
+ }
+
+ [PSCustomObject]@{
+ Id = $Item.id
+ HwAddr = $Item.hwaddr
+ Interface = $Item.interface
+ FirstSeen = (Convert-PiHoleUnixTimeToLocalTime -UnixTime $Item.firstSeen).LocalTime
+ LastQuery = (Convert-PiHoleUnixTimeToLocalTime -UnixTime $Item.lastQuery).LocalTime
+ NumQueries = $Item.numQueries
+ MacVendor = $Item.macVendor
+ Ips = $Ips
+ }
+ }
+ Write-Output $ObjectFinal
+ }
+ }
+
+ catch {
+ Write-Error -Message $_.Exception.Message
+ }
+
+ finally {
+ if ($Sid) {
+ Remove-PiHoleCurrentAuthSession -PiHoleServer $PiHoleServer -Sid $Sid -IgnoreSsl $IgnoreSsl
+ }
+ }
+}
diff --git a/PiHoleShell/Public/NetworkInformation/Get-PiHoleNetworkGateway.ps1 b/PiHoleShell/Public/NetworkInformation/Get-PiHoleNetworkGateway.ps1
new file mode 100644
index 0000000..ee6c1d3
--- /dev/null
+++ b/PiHoleShell/Public/NetworkInformation/Get-PiHoleNetworkGateway.ps1
@@ -0,0 +1,77 @@
+function Get-PiHoleNetworkGateway {
+ <#
+.SYNOPSIS
+Get info about the gateway of your Pi-hole
+
+.DESCRIPTION
+Returns information about your Pi-hole's network gateway(s). The response shape varies by
+address family and interface type, so it's returned as nested objects with PascalCase property
+names rather than a hand-picked subset - use -RawOutput to see the untouched API response.
+
+.PARAMETER PiHoleServer
+The URL to the PiHole Server, for example "http://pihole.domain.com:8080", or "http://192.168.1.100"
+
+.PARAMETER Password
+The API Password you generated from your PiHole server
+
+.PARAMETER Detailed
+Include detailed information about the individual interfaces and routes. Available fields
+depend on the interface type and state
+
+.PARAMETER IgnoreSsl
+Set to $true to skip SSL certificate validation
+
+.PARAMETER RawOutput
+This will dump the response instead of the formatted object
+
+.EXAMPLE
+Get-PiHoleNetworkGateway -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password"
+ #>
+ [CmdletBinding(HelpUri = 'https://ftl.pi-hole.net/master/docs/#get-/network/gateway')]
+ [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")]
+ param (
+ [Parameter(Mandatory = $true)]
+ [System.URI]$PiHoleServer,
+ [Parameter(Mandatory = $true)]
+ [string]$Password,
+ [Nullable[bool]]$Detailed,
+ [bool]$IgnoreSsl = $false,
+ [bool]$RawOutput = $false
+ )
+ try {
+ $Sid = Request-PiHoleAuth -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl
+
+ $Uri = "$($PiHoleServer.OriginalString)/api/network/gateway"
+ if ($PSBoundParameters.ContainsKey('Detailed')) {
+ $Uri += "?detailed=$($Detailed.ToString().ToLower())"
+ }
+
+ $Params = @{
+ Headers = @{sid = $($Sid) }
+ Uri = $Uri
+ Method = "Get"
+ SkipCertificateCheck = $IgnoreSsl
+ ContentType = "application/json"
+ }
+
+ $Response = Invoke-RestMethod @Params
+
+ if ($RawOutput) {
+ Write-Output $Response
+ }
+ else {
+ $Object = ConvertTo-PiHolePascalCaseObject -InputObject $Response.gateway
+ Write-Output $Object
+ }
+ }
+
+ catch {
+ Write-Error -Message $_.Exception.Message
+ }
+
+ finally {
+ if ($Sid) {
+ Remove-PiHoleCurrentAuthSession -PiHoleServer $PiHoleServer -Sid $Sid -IgnoreSsl $IgnoreSsl
+ }
+ }
+}
diff --git a/PiHoleShell/Public/NetworkInformation/Get-PiHoleNetworkInterface.ps1 b/PiHoleShell/Public/NetworkInformation/Get-PiHoleNetworkInterface.ps1
new file mode 100644
index 0000000..8efbc42
--- /dev/null
+++ b/PiHoleShell/Public/NetworkInformation/Get-PiHoleNetworkInterface.ps1
@@ -0,0 +1,77 @@
+function Get-PiHoleNetworkInterface {
+ <#
+.SYNOPSIS
+Get info about the interfaces of your Pi-hole
+
+.DESCRIPTION
+Returns information about your Pi-hole's networking interfaces. Not every field applies to
+every interface type, so the response is returned as nested objects with PascalCase property
+names rather than a hand-picked subset - use -RawOutput to see the untouched API response.
+
+.PARAMETER PiHoleServer
+The URL to the PiHole Server, for example "http://pihole.domain.com:8080", or "http://192.168.1.100"
+
+.PARAMETER Password
+The API Password you generated from your PiHole server
+
+.PARAMETER Detailed
+Include more detailed information about the individual interfaces. Available fields depend on
+the interface type and state
+
+.PARAMETER IgnoreSsl
+Set to $true to skip SSL certificate validation
+
+.PARAMETER RawOutput
+This will dump the response instead of the formatted object
+
+.EXAMPLE
+Get-PiHoleNetworkInterface -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password"
+ #>
+ [CmdletBinding(HelpUri = 'https://ftl.pi-hole.net/master/docs/#get-/network/interfaces')]
+ [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")]
+ param (
+ [Parameter(Mandatory = $true)]
+ [System.URI]$PiHoleServer,
+ [Parameter(Mandatory = $true)]
+ [string]$Password,
+ [Nullable[bool]]$Detailed,
+ [bool]$IgnoreSsl = $false,
+ [bool]$RawOutput = $false
+ )
+ try {
+ $Sid = Request-PiHoleAuth -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl
+
+ $Uri = "$($PiHoleServer.OriginalString)/api/network/interfaces"
+ if ($PSBoundParameters.ContainsKey('Detailed')) {
+ $Uri += "?detailed=$($Detailed.ToString().ToLower())"
+ }
+
+ $Params = @{
+ Headers = @{sid = $($Sid) }
+ Uri = $Uri
+ Method = "Get"
+ SkipCertificateCheck = $IgnoreSsl
+ ContentType = "application/json"
+ }
+
+ $Response = Invoke-RestMethod @Params
+
+ if ($RawOutput) {
+ Write-Output $Response
+ }
+ else {
+ $Object = ConvertTo-PiHolePascalCaseObject -InputObject $Response.interfaces
+ Write-Output $Object
+ }
+ }
+
+ catch {
+ Write-Error -Message $_.Exception.Message
+ }
+
+ finally {
+ if ($Sid) {
+ Remove-PiHoleCurrentAuthSession -PiHoleServer $PiHoleServer -Sid $Sid -IgnoreSsl $IgnoreSsl
+ }
+ }
+}
diff --git a/PiHoleShell/Public/NetworkInformation/Get-PiHoleNetworkRoute.ps1 b/PiHoleShell/Public/NetworkInformation/Get-PiHoleNetworkRoute.ps1
new file mode 100644
index 0000000..e6f3740
--- /dev/null
+++ b/PiHoleShell/Public/NetworkInformation/Get-PiHoleNetworkRoute.ps1
@@ -0,0 +1,77 @@
+function Get-PiHoleNetworkRoute {
+ <#
+.SYNOPSIS
+Get info about the routes of your Pi-hole
+
+.DESCRIPTION
+Returns information about your Pi-hole's networking routes. Not every field applies to every
+route type, so the response is returned as nested objects with PascalCase property names
+rather than a hand-picked subset - use -RawOutput to see the untouched API response.
+
+.PARAMETER PiHoleServer
+The URL to the PiHole Server, for example "http://pihole.domain.com:8080", or "http://192.168.1.100"
+
+.PARAMETER Password
+The API Password you generated from your PiHole server
+
+.PARAMETER Detailed
+Include more detailed information about the individual routes. Available fields depend on the
+route type and state
+
+.PARAMETER IgnoreSsl
+Set to $true to skip SSL certificate validation
+
+.PARAMETER RawOutput
+This will dump the response instead of the formatted object
+
+.EXAMPLE
+Get-PiHoleNetworkRoute -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password"
+ #>
+ [CmdletBinding(HelpUri = 'https://ftl.pi-hole.net/master/docs/#get-/network/routes')]
+ [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")]
+ param (
+ [Parameter(Mandatory = $true)]
+ [System.URI]$PiHoleServer,
+ [Parameter(Mandatory = $true)]
+ [string]$Password,
+ [Nullable[bool]]$Detailed,
+ [bool]$IgnoreSsl = $false,
+ [bool]$RawOutput = $false
+ )
+ try {
+ $Sid = Request-PiHoleAuth -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl
+
+ $Uri = "$($PiHoleServer.OriginalString)/api/network/routes"
+ if ($PSBoundParameters.ContainsKey('Detailed')) {
+ $Uri += "?detailed=$($Detailed.ToString().ToLower())"
+ }
+
+ $Params = @{
+ Headers = @{sid = $($Sid) }
+ Uri = $Uri
+ Method = "Get"
+ SkipCertificateCheck = $IgnoreSsl
+ ContentType = "application/json"
+ }
+
+ $Response = Invoke-RestMethod @Params
+
+ if ($RawOutput) {
+ Write-Output $Response
+ }
+ else {
+ $Object = ConvertTo-PiHolePascalCaseObject -InputObject $Response.routes
+ Write-Output $Object
+ }
+ }
+
+ catch {
+ Write-Error -Message $_.Exception.Message
+ }
+
+ finally {
+ if ($Sid) {
+ Remove-PiHoleCurrentAuthSession -PiHoleServer $PiHoleServer -Sid $Sid -IgnoreSsl $IgnoreSsl
+ }
+ }
+}
diff --git a/PiHoleShell/Public/NetworkInformation/Remove-PiHoleNetworkDevice.ps1 b/PiHoleShell/Public/NetworkInformation/Remove-PiHoleNetworkDevice.ps1
new file mode 100644
index 0000000..aac1ee4
--- /dev/null
+++ b/PiHoleShell/Public/NetworkInformation/Remove-PiHoleNetworkDevice.ps1
@@ -0,0 +1,80 @@
+function Remove-PiHoleNetworkDevice {
+ <#
+.SYNOPSIS
+Delete a device from the network table
+
+.DESCRIPTION
+Deletes a device from Pi-hole's network table, along with all its associated IP addresses and
+hostnames. Use Get-PiHoleNetworkDevice to find the device's Id first.
+
+.PARAMETER PiHoleServer
+The URL to the PiHole Server, for example "http://pihole.domain.com:8080", or "http://192.168.1.100"
+
+.PARAMETER Password
+The API Password you generated from your PiHole server
+
+.PARAMETER DeviceId
+The Id of the device to remove, as shown by Get-PiHoleNetworkDevice
+
+.PARAMETER IgnoreSsl
+Set to $true to skip SSL certificate validation
+
+.PARAMETER RawOutput
+This will dump the response instead of the formatted object
+
+.EXAMPLE
+Remove-PiHoleNetworkDevice -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" -DeviceId 5
+ #>
+ [CmdletBinding(SupportsShouldProcess = $true, HelpUri = 'https://ftl.pi-hole.net/master/docs/#delete-/network/devices/-id-')]
+ [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")]
+ param (
+ [Parameter(Mandatory = $true)]
+ [System.URI]$PiHoleServer,
+ [Parameter(Mandatory = $true)]
+ [string]$Password,
+ [Parameter(Mandatory = $true)]
+ [int]$DeviceId,
+ [bool]$IgnoreSsl = $false,
+ [bool]$RawOutput = $false
+ )
+ try {
+ $Target = "Pi-Hole network device $DeviceId"
+ if ($PSCmdlet.ShouldProcess($Target, "Remove device")) {
+ $Sid = Request-PiHoleAuth -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl
+
+ $Params = @{
+ Headers = @{sid = $($Sid) }
+ Uri = "$($PiHoleServer.OriginalString)/api/network/devices/$DeviceId"
+ Method = "Delete"
+ SkipCertificateCheck = $IgnoreSsl
+ ContentType = "application/json"
+ }
+
+ $Response = Invoke-RestMethod @Params
+
+ if ($RawOutput) {
+ Write-Output $Response
+ }
+
+ else {
+ # A successful delete returns 204 No Content, so there's no response body to
+ # build a rich object from.
+ $Object = [PSCustomObject]@{
+ DeviceId = $DeviceId
+ Status = "Removed"
+ }
+ Write-Output $Object
+ }
+ }
+ }
+
+ catch {
+ Write-Error -Message $_.Exception.Message
+ }
+
+ finally {
+ if ($Sid) {
+ Remove-PiHoleCurrentAuthSession -PiHoleServer $PiHoleServer -Sid $Sid -IgnoreSsl $IgnoreSsl
+ }
+ }
+}
diff --git a/README.md b/README.md
index 37671ed..d1cf8e7 100644
--- a/README.md
+++ b/README.md
@@ -130,6 +130,16 @@ See [docs/EXAMPLES.md](docs/EXAMPLES.md) for real, captured output from every fu
| `Remove-PiHoleDomain` | Remove a domain |
| `Update-PiHoleDomain` | Update a domain |
+### Client Management
+
+| Function | Description |
+|---|---|
+| `Get-PiHoleClient` | Get clients |
+| `Get-PiHoleClientSuggestion` | Get client suggestions |
+| `New-PiHoleClient` | Add a new client |
+| `Remove-PiHoleClient` | Remove a client |
+| `Update-PiHoleClient` | Update a client |
+
### Metrics
| Function | Description |
@@ -168,9 +178,14 @@ See [docs/EXAMPLES.md](docs/EXAMPLES.md) for real, captured output from every fu
| `Get-PiHoleInfoSystem` | Get info about various system parameters |
| `Get-PiHoleInfoVersion` | Get Pi-hole version |
| `Get-PiHoleLogWebserver` | Get webserver log content |
+| `Get-PiHoleNetworkDevice` | Get info about the devices in your local network as seen by your Pi-hole |
+| `Get-PiHoleNetworkGateway` | Get info about the gateway of your Pi-hole |
+| `Get-PiHoleNetworkInterface` | Get info about the interfaces of your Pi-hole |
+| `Get-PiHoleNetworkRoute` | Get info about the routes of your Pi-hole |
| `Get-PiHolePadd` | Get summarized data for PADD |
| `Get-PiHoleTeleporterDownload` | Export Pi-hole settings |
| `Remove-PiHoleInfoMessage` | Delete a Pi-hole diagnosis message |
+| `Remove-PiHoleNetworkDevice` | Delete a device from the network table |
### Authentication
diff --git a/docs/EXAMPLES.md b/docs/EXAMPLES.md
index a0deb07..1cc2273 100644
--- a/docs/EXAMPLES.md
+++ b/docs/EXAMPLES.md
@@ -100,8 +100,8 @@ Name : PiHoleShellDocsExampleGroup
Comment : Example group
Enabled : True
Id : 4
-DateAdded : 9/27/2026 7:45:33 AM
-DateModified : 9/27/2026 7:45:33 AM
+DateAdded : 9/27/2026 11:54:34 AM
+DateModified : 9/27/2026 11:54:34 AM
```
### Get-PiHoleGroup
@@ -137,8 +137,8 @@ Name : PiHoleShellDocsExampleGroup
Comment : Example group
Enabled : True
Id : 4
-DateAdded : 9/27/2026 7:45:33 AM
-DateModified : 9/27/2026 7:45:33 AM
+DateAdded : 9/27/2026 11:54:34 AM
+DateModified : 9/27/2026 11:54:34 AM
```
### Update-PiHoleGroup
@@ -153,8 +153,8 @@ Name : PiHoleShellDocsExampleGroup
Comment : Example group
Enabled : False
Id : 4
-DateAdded : 9/27/2026 7:45:33 AM
-DateModified : 9/27/2026 7:45:51 AM
+DateAdded : 9/27/2026 11:54:34 AM
+DateModified : 9/27/2026 11:55:06 AM
```
### Remove-PiHoleGroup
@@ -184,8 +184,8 @@ Comment : Example list
Groups : {Default}
Enabled : True
Id : 65
-DateAdded : 9/27/2026 7:46:28 AM
-DateModified : 9/27/2026 7:46:28 AM
+DateAdded : 9/27/2026 11:56:17 AM
+DateModified : 9/27/2026 11:56:17 AM
Type : Block
DateUpdated :
Number : 0
@@ -214,7 +214,7 @@ DateUpdated : 7/6/2025 2:20:13 AM
Number : 74761
InvalidDomains : 1
AbpEntries : 0
-Status : 1
+Status : 2
Address : https://adaway.org/hosts.txt
Comment :
@@ -288,8 +288,8 @@ Comment : Example list
Groups : {Default}
Enabled : False
Id : 65
-DateAdded : 9/27/2026 7:46:28 AM
-DateModified : 9/27/2026 7:47:01 AM
+DateAdded : 9/27/2026 11:56:17 AM
+DateModified : 9/27/2026 11:57:19 AM
Type : Block
DateUpdated :
Number : 0
@@ -319,7 +319,7 @@ DateUpdated : 9/26/2026 11:28:08 PM
Number : 74761
InvalidDomains : 1
AbpEntries : 0
-Status : 1
+Status : 2
Domain : doubleclick.net
Address : https://adaway.org/hosts.txt
@@ -414,9 +414,9 @@ Kind : Exact
Comment : Example domain
Groups : {Default}
Enabled : True
-Id : 22
-DateAdded : 9/27/2026 7:47:39 AM
-DateModified : 9/27/2026 7:47:39 AM
+Id : 12
+DateAdded : 9/27/2026 11:58:32 AM
+DateModified : 9/27/2026 11:58:32 AM
```
### Get-PiHoleDomain
@@ -434,9 +434,20 @@ Kind : exact
Comment : Example domain
Groups : {Default}
Enabled : True
-Id : 22
-DateAdded : 9/27/2026 7:47:39 AM
-DateModified : 9/27/2026 7:47:39 AM
+Id : 12
+DateAdded : 9/27/2026 11:58:32 AM
+DateModified : 9/27/2026 11:58:32 AM
+
+Domain : piholeshell-test-domain.example.com
+Unicode : piholeshell-test-domain.example.com
+Type : allow
+Kind : exact
+Comment :
+Groups : {Default}
+Enabled : True
+Id : 13
+DateAdded : 9/27/2026 11:58:40 AM
+DateModified : 9/27/2026 11:58:40 AM
```
### Update-PiHoleDomain
@@ -454,9 +465,9 @@ Kind : Exact
Comment : Example domain
Groups : {Default}
Enabled : False
-Id : 22
-DateAdded : 9/27/2026 7:47:39 AM
-DateModified : 9/27/2026 7:48:12 AM
+Id : 12
+DateAdded : 9/27/2026 11:58:32 AM
+DateModified : 9/27/2026 11:59:34 AM
```
### Remove-PiHoleDomain
@@ -473,6 +484,81 @@ Kind : Exact
Status : Removed
```
+## Client Management
+
+### New-PiHoleClient
+
+```powershell
+New-PiHoleClient -PiHoleServer $PiHoleServer -Password $Password -Client "192.168.99.99" -Comment "Example client"
+```
+
+```
+
+Client : 192.168.99.99
+Name :
+Comment : Example client
+Groups : {Default}
+Id : 1
+DateAdded : 9/27/2026 12:00:33 PM
+DateModified : 9/27/2026 12:00:33 PM
+```
+
+### Get-PiHoleClient
+
+```powershell
+Get-PiHoleClient -PiHoleServer $PiHoleServer -Password $Password
+```
+
+```
+
+Client : 192.168.99.99
+Name :
+Comment : Example client
+Groups : {Default}
+Id : 1
+DateAdded : 9/27/2026 12:00:33 PM
+DateModified : 9/27/2026 12:00:33 PM
+```
+
+### Get-PiHoleClientSuggestion
+
+```powershell
+Get-PiHoleClientSuggestion -PiHoleServer $PiHoleServer -Password $Password
+```
+
+```
+(no output)
+```
+
+### Update-PiHoleClient
+
+```powershell
+Update-PiHoleClient -PiHoleServer $PiHoleServer -Password $Password -Client "192.168.99.99" -Comment "Updated comment"
+```
+
+```
+
+Client : 192.168.99.99
+Name :
+Comment : Updated comment
+Groups : {Default}
+Id : 1
+DateAdded : 9/27/2026 12:00:33 PM
+DateModified : 9/27/2026 12:01:45 PM
+```
+
+### Remove-PiHoleClient
+
+```powershell
+Remove-PiHoleClient -PiHoleServer $PiHoleServer -Password $Password -Client "192.168.99.99"
+```
+
+```
+
+Client : 192.168.99.99
+Status : Removed
+```
+
## Metrics
### Get-PiHoleStatsSummary
@@ -483,18 +569,18 @@ Get-PiHoleStatsSummary -PiHoleServer $PiHoleServer -Password $Password
```
-Total : 281
-Blocked : 84
-PercentBlocked : 29.893238067627
-UniqueDomains : 21
-Forwarded : 37
-Cached : 160
+Total : 0
+Blocked : 0
+PercentBlocked : 0
+UniqueDomains : 0
+Forwarded : 0
+Cached : 0
Frequency : 0
-Types : @{A=130; AAAA=130; ANY=0; SRV=0; SOA=0; PTR=21; TXT=0; NAPTR=0; MX=0; DS=0; RRSIG=0; DNSKEY=0; NS=0; SVCB=0; HTTPS=0; OTHER=0}
-Status : @{Unknown=0; Gravity=84; Forwarded=37; Cache=139; Regex=0; DenyList=0; ExternalBlockedIp=0; ExternalBlockedNull=0; ExternalBlockedNxra=0; GravityCname=0; RegexCname=0; DenyListCname=0; Retired=0; RetiredDnssec=0; InProgress=0; Dbbusy=0; SpecialDomain=0; CacheStale=21; ExternalBlockedEde15=0}
-Replies : @{Unknown=12; Nodata=17; Nxdomain=3; Cname=0; Ip=231; Domain=18; Rrname=0; ServFail=0; Refused=0; Notimp=0; Other=0; Dnssec=0; None=0; Blob=0}
-Clients : @{Active=2; Total=2}
-Gravity : @{DomainsBeingBlocked=499226; LastUpdate=1790483337}
+Types : @{A=0; AAAA=0; ANY=0; SRV=0; SOA=0; PTR=0; TXT=0; NAPTR=0; MX=0; DS=0; RRSIG=0; DNSKEY=0; NS=0; SVCB=0; HTTPS=0; OTHER=0}
+Status : @{Unknown=0; Gravity=0; Forwarded=0; Cache=0; Regex=0; DenyList=0; ExternalBlockedIp=0; ExternalBlockedNull=0; ExternalBlockedNxra=0; GravityCname=0; RegexCname=0; DenyListCname=0; Retired=0; RetiredDnssec=0; InProgress=0; Dbbusy=0; SpecialDomain=0; CacheStale=0; ExternalBlockedEde15=0}
+Replies : @{Unknown=0; Nodata=0; Nxdomain=0; Cname=0; Ip=0; Domain=0; Rrname=0; ServFail=0; Refused=0; Notimp=0; Other=0; Dnssec=0; None=0; Blob=0}
+Clients : @{Active=0; Total=0}
+Gravity : @{DomainsBeingBlocked=496171; LastUpdate=1790527593}
```
### Get-PiHoleStatsRecentBlocked
@@ -504,8 +590,7 @@ Get-PiHoleStatsRecentBlocked -PiHoleServer $PiHoleServer -Password $Password
```
```
-
-Blocked : googleadservices.com
+(no output)
```
### Get-PiHoleStatsQueryType
@@ -517,10 +602,10 @@ Get-PiHoleStatsQueryType -PiHoleServer $PiHoleServer -Password $Password
```
Type : A
-Count : 130
+Count : 0
Type : AAAA
-Count : 130
+Count : 0
Type : ANY
Count : 0
@@ -541,23 +626,7 @@ Get-PiHoleStatsTopDomain -PiHoleServer $PiHoleServer -Password $Password
```
```
-
-Domain : wikipedia.org
-Count : 20
-
-Domain : bbc.com
-Count : 18
-
-Domain : python.org
-Count : 18
-
-Domain : example.com
-Count : 18
-
-Domain : mozilla.org
-Count : 16
-
-_(showing 5 of 10 results)_
+(no output)
```
### Get-PiHoleStatsTopClient
@@ -567,14 +636,7 @@ Get-PiHoleStatsTopClient -PiHoleServer $PiHoleServer -Password $Password
```
```
-
-IP : 192.168.1.162
-Name :
-Count : 260
-
-IP : 127.0.0.1
-Name : localhost
-Count : 21
+(no output)
```
### Get-PiHoleStatsUpstream
@@ -585,9 +647,9 @@ Get-PiHoleStatsUpstream -PiHoleServer $PiHoleServer -Password $Password
```
-TotalQueries : 281
-ForwardedQueries : 37
-Upstreams : {@{Ip=blocklist; Name=blocklist; Port=-1; Count=84; ResponseTime=0; Variance=0}, @{Ip=cache; Name=cache; Port=-1; Count=160; ResponseTime=0; Variance=0}, @{Ip=8.8.8.8; Name=dns.google; Port=53; Count=37; ResponseTime=0.024111141761144; Variance=0.00213313327521311}}
+TotalQueries : 0
+ForwardedQueries : 0
+Upstreams : {@{Ip=blocklist; Name=blocklist; Port=-1; Count=0; ResponseTime=0; Variance=0}, @{Ip=cache; Name=cache; Port=-1; Count=0; ResponseTime=0; Variance=0}}
```
### Get-PiHoleStatsQuerySuggestions
@@ -598,10 +660,10 @@ Get-PiHoleStatsQuerySuggestions -PiHoleServer $PiHoleServer -Password $Password
```
-Domain : {wikipedia.org, bbc.com, python.org, example.com…}
-ClientIp : {192.168.1.162, 127.0.0.1}
-ClientName : {localhost}
-Upstream : {blocklist, cache, 8.8.8.8#53 (dns.google), permitted}
+Domain : {}
+ClientIp : {}
+ClientName : {}
+Upstream : {blocklist, cache, permitted}
Type : {A, AAAA, ANY, SRV…}
Status : {UNKNOWN, GRAVITY, FORWARDED, CACHE…}
Reply : {UNKNOWN, NODATA, NXDOMAIN, CNAME…}
@@ -618,10 +680,10 @@ Get-PiHoleStatsDatabaseSummary -PiHoleServer $PiHoleServer -Password $Password
```
-SumQueries : 8
+SumQueries : 0
SumBlocked : 0
PercentBlocked : 0
-TotalClients : 1
+TotalClients : 0
```
### Get-PiHoleStatsDatabaseQueryType
@@ -647,7 +709,7 @@ Type : SRV
Count : 0
Type : SOA
-Count : 8
+Count : 0
_(showing 5 of 16 results)_
```
@@ -661,9 +723,7 @@ Get-PiHoleStatsDatabaseTopDomain -PiHoleServer $PiHoleServer -Password $Password
```
```
-
-Domain : 1.0.0.127.in-addr.arpa
-Count : 8
+(no output)
```
### Get-PiHoleStatsDatabaseTopClient
@@ -675,10 +735,7 @@ Get-PiHoleStatsDatabaseTopClient -PiHoleServer $PiHoleServer -Password $Password
```
```
-
-IP : 127.0.0.1
-Name : localhost
-Count : 8
+(no output)
```
### Get-PiHoleStatsDatabaseUpstream
@@ -691,9 +748,9 @@ Get-PiHoleStatsDatabaseUpstream -PiHoleServer $PiHoleServer -Password $Password
```
-TotalQueries : 8
+TotalQueries : 0
ForwardedQueries : 0
-Upstreams : {@{Ip=cache; Name=cache; Port=-1; Count=8; ResponseTime=0; Variance=0}, @{Ip=blocklist; Name=blocklist; Port=-1; Count=0; ResponseTime=0; Variance=0}}
+Upstreams : {@{Ip=cache; Name=cache; Port=-1; Count=0; ResponseTime=0; Variance=0}, @{Ip=blocklist; Name=blocklist; Port=-1; Count=0; ResponseTime=0; Variance=0}}
```
## Configuration & Diagnostics
@@ -727,22 +784,22 @@ Get-PiHolePadd -PiHoleServer $PiHoleServer -Password $Password
CpuPercent : 0
MemoryPercent : 0
-ActiveClients : 2
+ActiveClients : 0
Blocking : enabled
-Cache : @{Size=10000; Inserted=96; Evicted=0}
+Cache : @{Size=10000; Inserted=0; Evicted=0}
Config : @{DhcpActive=False; DhcpStart=; DhcpEnd=; DhcpIpv6=False; DnsDnssec=False; DnsDomain=lan; DnsNumUpstreams=2; DnsPort=53; DnsrevServerAactive=False; PrivacyLevel=0}
-GravitySize : 499226
+GravitySize : 496171
HostModel : Raspberry Pi Zero W Rev 1.1
IFace : @{v4=; v6=}
NodeName : dns3.localdomain
Pid : 389
-Queries : @{Total=281; Blocked=84; PercentBlocked=29.893238067627; QueryFrequency=0}
-RecentBlocked : googleadservices.com
-Sensors : @{CpuTemp=36.318; HotLimit=60; Unit=C}
-System : @{Uptime=629874; Memory=; Procs=70; Cpu=; Ftl=}
-TopBlocked : googleadservices.com
-TopClient : 192.168.1.162
-TopDomain : wikipedia.org
+Queries : @{Total=0; Blocked=0; PercentBlocked=0; QueryFrequency=0}
+RecentBlocked :
+Sensors : @{CpuTemp=40.084; HotLimit=60; Unit=C}
+System : @{Uptime=644738; Memory=; Procs=71; Cpu=; Ftl=}
+TopBlocked :
+TopClient :
+TopDomain :
Version : @{Core=; Web=; Ftl=; Docker=}
```
@@ -779,11 +836,11 @@ Get-PiHoleInfoSystem -PiHoleServer $PiHoleServer -Password $Password
```
-Uptime : 629884
+Uptime : 644757
Memory : @{Ram=; Swap=}
-Procs : 70
-Cpu : @{NumProcessors=1; PercentCpu=96.8000030517578; Load=}
-Ftl : @{PercentMemory=2.40430068969727; PercentCpu=96.3000030517578}
+Procs : 71
+Cpu : @{NumProcessors=1; PercentCpu=100; Load=}
+Ftl : @{PercentMemory=2.33196616172791; PercentCpu=99.0999984741211}
```
### Get-PiHoleInfoFtl
@@ -794,16 +851,16 @@ Get-PiHoleInfoFtl -PiHoleServer $PiHoleServer -Password $Password
```
-Database : @{Gravity=499226; Antigravity=0; Groups=3; Lists=14; Clients=0; Domains=; Regex=}
+Database : @{Gravity=496171; Antigravity=0; Groups=3; Lists=14; Clients=0; Domains=; Regex=}
PrivacyLevel : 0
QueryFrequency : 0
-Clients : @{Total=2; Active=2}
+Clients : @{Total=0; Active=0}
Pid : 389
-Uptime : 51790377.711423
-PercentMemory : 2.40430068969727
-PercentCpu : 96.3000030517578
+Uptime : 495351.278596
+PercentMemory : 2.32703423500061
+PercentCpu : 99.3000030517578
AllowDestructive : True
-Dnsmasq : @{DnsCacheInserted=96; DnsCacheLiveFreed=0; DnsQueriesForwarded=58; DnsAuthAnswered=0; DnsLocalAnswered=223; DnsStaleAnswered=21; DnsUnanswered=0; DnssecMaxCryptoUse=0; DnssecMaxSigFail=0; DnssecMaxWork=0; Bootp=0; Pxe=0; DhcpAck=0; DhcpDecline=0; DhcpDiscover=0; DhcpInform=0; DhcpNak=0; DhcpOffer=0; DhcpRelease=0; DhcpRequest=0; Noanswer=0; LeasesAllocated4=0; LeasesPruned4=0; LeasesAllocated6=0; LeasesPruned6=0; TcpConnections=0; DhcpLeasequery=0; DhcpLeaseUnassigned=0; DhcpLeaseActve=0; DhcpLeaseUnknown=0}
+Dnsmasq : @{DnsCacheInserted=0; DnsCacheLiveFreed=0; DnsQueriesForwarded=0; DnsAuthAnswered=0; DnsLocalAnswered=0; DnsStaleAnswered=0; DnsUnanswered=0; DnssecMaxCryptoUse=0; DnssecMaxSigFail=0; DnssecMaxWork=0; Bootp=0; Pxe=0; DhcpAck=0; DhcpDecline=0; DhcpDiscover=0; DhcpInform=0; DhcpNak=0; DhcpOffer=0; DhcpRelease=0; DhcpRequest=0; Noanswer=0; LeasesAllocated4=0; LeasesPruned4=0; LeasesAllocated6=0; LeasesPruned6=0; TcpConnections=0; DhcpLeasequery=0; DhcpLeaseUnassigned=0; DhcpLeaseActve=0; DhcpLeaseUnknown=0}
```
### Get-PiHoleInfoSensors
@@ -815,7 +872,7 @@ Get-PiHoleInfoSensors -PiHoleServer $PiHoleServer -Password $Password
```
List : {@{Name=cpu_thermal; Path=hwmon0; Source=devices/virtual/thermal/thermal_zone0; Temps=}, @{Name=rpi_volt; Path=hwmon1; Source=devices/platform/soc/soc:firmware/raspberrypi-hwmon; Temps=}}
-CpuTemp : 36.856
+CpuTemp : 40.622
HotLimit : 60
Unit : C
```
@@ -832,12 +889,12 @@ Size : 13983744
Type : Regular file
Mode : rw-r-----
AccessTime : 6/25/2025 12:32:44 AM
-ModifiedTime : 9/27/2026 4:04:00 AM
-ChangeTime : 9/27/2026 4:04:00 AM
+ModifiedTime : 9/27/2026 11:44:09 AM
+ChangeTime : 9/27/2026 11:44:09 AM
Owner : @{User=; Group=}
-Queries : 281
-EarliestTimestamp : 9/26/2026 7:40:00 AM
-QueriesDisk : 281
+Queries : 0
+EarliestTimestamp : 9/26/2026 11:50:00 AM
+QueriesDisk : 0
EarliestTimestampDisk :
SqliteVersion : 3.53.1
```
@@ -905,7 +962,12 @@ Get-PiHoleInfoMessage -PiHoleServer $PiHoleServer -Password $Password
```
```
-(no output)
+
+Id : 1
+Timestamp : 9/27/2026 11:49:11 AM
+Type : LOAD
+Plain : Long-term load (15min avg) larger than number of processors: 1.3 > 1
+Html : Long-term load (15min avg) larger than number of processors: 1.3 > 1
This may slow down DNS resolution and can cause bottlenecks.
```
### Get-PiHoleInfoMessageCount
@@ -916,7 +978,7 @@ Get-PiHoleInfoMessageCount -PiHoleServer $PiHoleServer -Password $Password
```
-Count : 0
+Count : 1
```
### Remove-PiHoleInfoMessage
@@ -940,7 +1002,7 @@ Get-PiHoleLogWebserver -PiHoleServer $PiHoleServer -Password $Password
```
-Log : @{Timestamp=9/26/2026 5:21:18 PM; Message=Initializing HTTP server on ports "8089,8489s"; Priority=}
+Log : @{Timestamp=9/27/2026 11:44:11 AM; Message=Initializing HTTP server on ports "8089,8489s"; Priority=}
NextID : 1
Pid : 389
File : /var/log/pihole/webserver.log
@@ -970,31 +1032,31 @@ Get-PiHoleHistory -PiHoleServer $PiHoleServer -Password $Password
```
-Timestamp : 9/26/2026 7:55:00 AM
+Timestamp : 9/26/2026 12:05:00 PM
Total : 0
Cached : 0
Blocked : 0
Forwarded : 0
-Timestamp : 9/26/2026 8:05:00 AM
+Timestamp : 9/26/2026 12:15:00 PM
Total : 0
Cached : 0
Blocked : 0
Forwarded : 0
-Timestamp : 9/26/2026 8:15:00 AM
+Timestamp : 9/26/2026 12:25:00 PM
Total : 0
Cached : 0
Blocked : 0
Forwarded : 0
-Timestamp : 9/26/2026 8:25:00 AM
+Timestamp : 9/26/2026 12:35:00 PM
Total : 0
Cached : 0
Blocked : 0
Forwarded : 0
-Timestamp : 9/26/2026 8:35:00 AM
+Timestamp : 9/26/2026 12:45:00 PM
Total : 0
Cached : 0
Blocked : 0
@@ -1011,20 +1073,20 @@ Get-PiHoleHistoryClient -PiHoleServer $PiHoleServer -Password $Password
```
-Timestamp : 9/26/2026 7:55:00 AM
-Clients : {@{IP=192.168.1.162; Name=; Count=0}, @{IP=others; Name=; Count=0}}
+Timestamp : 9/26/2026 12:05:00 PM
+Clients : @{IP=others; Name=; Count=0}
-Timestamp : 9/26/2026 8:05:00 AM
-Clients : {@{IP=192.168.1.162; Name=; Count=0}, @{IP=others; Name=; Count=0}}
+Timestamp : 9/26/2026 12:15:00 PM
+Clients : @{IP=others; Name=; Count=0}
-Timestamp : 9/26/2026 8:15:00 AM
-Clients : {@{IP=192.168.1.162; Name=; Count=0}, @{IP=others; Name=; Count=0}}
+Timestamp : 9/26/2026 12:25:00 PM
+Clients : @{IP=others; Name=; Count=0}
-Timestamp : 9/26/2026 8:25:00 AM
-Clients : {@{IP=192.168.1.162; Name=; Count=0}, @{IP=others; Name=; Count=0}}
+Timestamp : 9/26/2026 12:35:00 PM
+Clients : @{IP=others; Name=; Count=0}
-Timestamp : 9/26/2026 8:35:00 AM
-Clients : {@{IP=192.168.1.162; Name=; Count=0}, @{IP=others; Name=; Count=0}}
+Timestamp : 9/26/2026 12:45:00 PM
+Clients : @{IP=others; Name=; Count=0}
_(showing 5 of 145 results)_
```
@@ -1037,67 +1099,154 @@ _Defaults to the last 8 hours; pass -From/-Until for a different window._
Get-PiHoleHistoryDatabase -PiHoleServer $PiHoleServer -Password $Password
```
+```
+(no output)
```
-Timestamp : 9/27/2026 12:00:00 AM
-Total : 1
-Cached : 1
-Blocked : 0
-Forwarded : 0
+### Get-PiHoleHistoryDatabaseClient
-Timestamp : 9/27/2026 1:00:00 AM
-Total : 1
-Cached : 1
-Blocked : 0
-Forwarded : 0
+_Defaults to the last 8 hours; pass -From/-Until for a different window._
-Timestamp : 9/27/2026 2:00:00 AM
-Total : 1
-Cached : 1
-Blocked : 0
-Forwarded : 0
+```powershell
+Get-PiHoleHistoryDatabaseClient -PiHoleServer $PiHoleServer -Password $Password
+```
-Timestamp : 9/27/2026 3:00:00 AM
-Total : 1
-Cached : 1
-Blocked : 0
-Forwarded : 0
+```
+(no output)
+```
-Timestamp : 9/27/2026 4:00:00 AM
-Total : 1
-Cached : 1
-Blocked : 0
-Forwarded : 0
+### Get-PiHoleNetworkGateway
-_(showing 5 of 8 results)_
+```powershell
+Get-PiHoleNetworkGateway -PiHoleServer $PiHoleServer -Password $Password
```
-### Get-PiHoleHistoryDatabaseClient
+```
-_Defaults to the last 8 hours; pass -From/-Until for a different window._
+Family : inet
+Interface : wlan0
+Address : 192.168.1.1
+Local : 192.168.1.248
+```
+
+### Get-PiHoleNetworkRoute
```powershell
-Get-PiHoleHistoryDatabaseClient -PiHoleServer $PiHoleServer -Password $Password
+Get-PiHoleNetworkRoute -PiHoleServer $PiHoleServer -Password $Password
+```
+
+```
+
+Table : 254
+Family : inet
+Protocol : boot
+Scope : universe
+Type : unicast
+Flags :
+Gateway : 192.168.1.1
+Oif : wlan0
+Dst : default
+
+Table : 254
+Family : inet
+Protocol : kernel
+Scope : link
+Type : unicast
+Flags :
+Dst : 192.168.1.0
+Prefsrc : 192.168.1.248
+Oif : wlan0
+
+Table : 255
+Family : inet
+Protocol : kernel
+Scope : host
+Type : local
+Flags :
+Dst : 127.0.0.0
+Prefsrc : 127.0.0.1
+Oif : lo
+
+Table : 255
+Family : inet
+Protocol : kernel
+Scope : host
+Type : local
+Flags :
+Dst : 127.0.0.1
+Prefsrc : 127.0.0.1
+Oif : lo
+
+Table : 255
+Family : inet
+Protocol : kernel
+Scope : link
+Type : broadcast
+Flags :
+Dst : 127.255.255.255
+Prefsrc : 127.0.0.1
+Oif : lo
+
+_(showing 5 of 11 results)_
+```
+
+### Get-PiHoleNetworkInterface
+
+```powershell
+Get-PiHoleNetworkInterface -PiHoleServer $PiHoleServer -Password $Password
```
```
-Timestamp : 9/27/2026 7:00:00 AM
-Clients : @{ClientId=3; Count=1}
+Name : lo
+Speed :
+Type : loopback
+Flags : {up, loopback, running, lower_up}
+State : unknown
+Carrier : True
+ProtoDown : False
+Address : 00:00:00:00:00:00
+Broadcast : 00:00:00:00:00:00
+Stats : @{RxBytes=; TxBytes=; Bits=64}
+Addresses : {@{Family=inet; Scope=host; Flags=permanent; Prefixlen=8; Address=127.0.0.1; AddressType=loopback; Local=127.0.0.1; LocalType=loopback; Label=lo; Prefered=4294967295; Valid=4294967295; Cstamp=1789883183.02; Tstamp=1789883183.02}, @{Family=inet6; Scope=host; Flags=permanent; Prefixlen=128; Address=::1; AddressType=loopback; Prefered=4294967295; Valid=4294967295; Cstamp=1789883183.02; Tstamp=1789883183.02}}
-Timestamp : 9/27/2026 6:00:00 AM
-Clients : @{ClientId=3; Count=1}
+Name : wlan0
+Speed :
+Type : ether
+Flags : {up, broadcast, running, multicast…}
+State : up
+Carrier : True
+ProtoDown : False
+Address : b8:27:eb:11:a4:d5
+Broadcast : ff:ff:ff:ff:ff:ff
+PermAddress : b8:27:eb:11:a4:d5
+Stats : @{RxBytes=; TxBytes=; Bits=64}
+Addresses : {@{Family=inet; Scope=universe; Flags=; Prefixlen=24; Address=192.168.1.248; AddressType=private; Local=192.168.1.248; LocalType=private; Broadcast=192.168.1.255; BroadcastType=private; Label=wlan0; Prefered=61931; Valid=61931; Cstamp=1789883203.32; Tstamp=1790074734.89}, @{Family=inet6; Scope=link; Flags=permanent; Prefixlen=64; Address=fe80::ba27:ebff:fe11:a4d5; AddressType=link-local (LL); Prefered=4294967295; Valid=4294967295; Cstamp=1789883202.81; Tstamp=1789883202.81}}
+```
-Timestamp : 9/27/2026 5:00:00 AM
-Clients : @{ClientId=3; Count=1}
+### Get-PiHoleNetworkDevice
-Timestamp : 9/27/2026 4:00:00 AM
-Clients : @{ClientId=3; Count=1}
+_Shows up to 10 devices by default; pass -MaxDevices for more._
-Timestamp : 9/27/2026 3:00:00 AM
-Clients : @{ClientId=3; Count=1}
+```powershell
+Get-PiHoleNetworkDevice -PiHoleServer $PiHoleServer -Password $Password
+```
-_(showing 5 of 8 results)_
+```
+(no output)
+```
+
+### Remove-PiHoleNetworkDevice
+
+_Network devices arise from Pi-hole having genuinely seen a device and cannot be manufactured on demand, so this example shows the error for a device ID that does not exist rather than a fabricated success._
+
+```powershell
+Remove-PiHoleNetworkDevice -PiHoleServer $PiHoleServer -Password $Password -DeviceId 5
+```
+
+```
+
+Error : Response status code does not indicate success: 404 (Not Found).
```
## Authentication
@@ -1111,13 +1260,41 @@ Get-PiHoleCurrentAuthSession -PiHoleServer $PiHoleServer -Password $Password
```
Id : 0
+CurrentSession : False
+Valid : True
+TlsLogin : True
+TlsMixed : False
+LoginAt : 9/27/2026 11:50:34 AM
+LastActive : 9/27/2026 11:50:34 AM
+ValidUntil : 9/27/2026 12:20:34 PM
+RemoteAddress : 192.168.1.162
+UserAgent : Mozilla/5.0 (Windows NT 10.0; Microsoft Windows 10.0.26200; en-US) PowerShell/7.6.6
+XForwardedFor :
+App : True
+Cli : False
+
+Id : 1
CurrentSession : True
Valid : True
TlsLogin : True
TlsMixed : False
-LoginAt : 9/27/2026 7:43:32 AM
-LastActive : 9/27/2026 7:43:36 AM
-ValidUntil : 9/27/2026 8:13:36 AM
+LoginAt : 9/27/2026 11:50:38 AM
+LastActive : 9/27/2026 11:50:47 AM
+ValidUntil : 9/27/2026 12:20:47 PM
+RemoteAddress : 192.168.1.162
+UserAgent : Mozilla/5.0 (Windows NT 10.0; Microsoft Windows 10.0.26200; en-US) PowerShell/7.6.6
+XForwardedFor :
+App : True
+Cli : False
+
+Id : 2
+CurrentSession : False
+Valid : True
+TlsLogin : True
+TlsMixed : False
+LoginAt : 9/27/2026 11:50:39 AM
+LastActive : 9/27/2026 11:50:39 AM
+ValidUntil : 9/27/2026 12:20:39 PM
RemoteAddress : 192.168.1.162
UserAgent : Mozilla/5.0 (Windows NT 10.0; Microsoft Windows 10.0.26200; en-US) PowerShell/7.6.6
XForwardedFor :
diff --git a/tests/ClientManagement/Get-PiHoleClient.Integration.Tests.ps1 b/tests/ClientManagement/Get-PiHoleClient.Integration.Tests.ps1
new file mode 100644
index 0000000..ef4ce5f
--- /dev/null
+++ b/tests/ClientManagement/Get-PiHoleClient.Integration.Tests.ps1
@@ -0,0 +1,62 @@
+# Requires -Module Pester
+#
+# Integration tests that call a REAL Pi-hole server. Configure tests/IntegrationConfig.local.ps1
+# (copy it from IntegrationConfig.example.ps1) before running. Tests are skipped automatically
+# if that file is missing.
+
+$script:ConfigAvailable = Test-Path (Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1')
+
+Describe 'Get-PiHoleClient (Integration)' -Tag 'Integration' {
+ BeforeAll {
+ Import-Module .\PiHoleShell\PiHoleShell.psm1 -Force
+
+ $script:TestClient = '192.168.99.99'
+
+ $configPath = Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1'
+ if (Test-Path $configPath) {
+ . $configPath
+ $script:PiHoleServer = $PiHoleServer
+ $script:PiHoleToken = $PiHoleToken
+ $script:PiHoleIgnoreSsl = $PiHoleIgnoreSsl
+
+ # Defensive cleanup in case a previous failed run left the test client behind
+ Remove-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient -Confirm:$false -ErrorAction SilentlyContinue -WarningAction SilentlyContinue | Out-Null
+
+ New-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient -Comment 'Pester integration test client' | Out-Null
+ }
+ }
+
+ AfterAll {
+ if ($script:PiHoleServer) {
+ Remove-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient -Confirm:$false -ErrorAction SilentlyContinue | Out-Null
+ }
+ }
+
+ It 'returns clients as formatted objects' -Skip:(-not $script:ConfigAvailable) {
+ $result = Get-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl
+ $result | Select-Object -First 5 | Format-Table | Out-String | Write-Host
+
+ $result | Should -Not -BeNullOrEmpty
+ }
+
+ It 'filters by Client' -Skip:(-not $script:ConfigAvailable) {
+ $result = Get-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient
+ $result | Format-List | Out-String | Write-Host
+
+ $result | Should -Not -BeNullOrEmpty
+ $result.Client | Should -Be $script:TestClient
+ }
+
+ It 'returns the raw API response when RawOutput is set' -Skip:(-not $script:ConfigAvailable) {
+ $result = Get-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -RawOutput $true
+ $result | Format-List | Out-String | Write-Host
+
+ $result | Should -Not -BeNullOrEmpty
+ }
+
+ It 'errors when given a bad password' -Skip:(-not $script:ConfigAvailable) {
+ $result = Get-PiHoleClient -PiHoleServer $script:PiHoleServer -Password 'definitely-not-the-real-token' -IgnoreSsl $script:PiHoleIgnoreSsl -ErrorVariable errOut -ErrorAction SilentlyContinue
+
+ $errOut | Should -Not -BeNullOrEmpty
+ }
+}
diff --git a/tests/ClientManagement/Get-PiHoleClientSuggestion.Integration.Tests.ps1 b/tests/ClientManagement/Get-PiHoleClientSuggestion.Integration.Tests.ps1
new file mode 100644
index 0000000..17d94ef
--- /dev/null
+++ b/tests/ClientManagement/Get-PiHoleClientSuggestion.Integration.Tests.ps1
@@ -0,0 +1,38 @@
+# Requires -Module Pester
+#
+# Integration tests that call a REAL Pi-hole server. Configure tests/IntegrationConfig.local.ps1
+# (copy it from IntegrationConfig.example.ps1) before running. Tests are skipped automatically
+# if that file is missing.
+
+$script:ConfigAvailable = Test-Path (Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1')
+
+Describe 'Get-PiHoleClientSuggestion (Integration)' -Tag 'Integration' {
+ BeforeAll {
+ Import-Module .\PiHoleShell\PiHoleShell.psm1 -Force
+
+ $configPath = Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1'
+ if (Test-Path $configPath) {
+ . $configPath
+ $script:PiHoleServer = $PiHoleServer
+ $script:PiHoleToken = $PiHoleToken
+ $script:PiHoleIgnoreSsl = $PiHoleIgnoreSsl
+ }
+ }
+
+ It 'succeeds and returns without throwing, even with no suggestions available' -Skip:(-not $script:ConfigAvailable) {
+ { Get-PiHoleClientSuggestion -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl } | Should -Not -Throw
+ }
+
+ It 'returns the raw API response when RawOutput is set' -Skip:(-not $script:ConfigAvailable) {
+ $result = Get-PiHoleClientSuggestion -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -RawOutput $true
+ $result | Format-List | Out-String | Write-Host
+
+ $result | Should -Not -BeNullOrEmpty
+ }
+
+ It 'errors when given a bad password' -Skip:(-not $script:ConfigAvailable) {
+ $result = Get-PiHoleClientSuggestion -PiHoleServer $script:PiHoleServer -Password 'definitely-not-the-real-token' -IgnoreSsl $script:PiHoleIgnoreSsl -ErrorVariable errOut -ErrorAction SilentlyContinue
+
+ $errOut | Should -Not -BeNullOrEmpty
+ }
+}
diff --git a/tests/ClientManagement/New-PiHoleClient.Integration.Tests.ps1 b/tests/ClientManagement/New-PiHoleClient.Integration.Tests.ps1
new file mode 100644
index 0000000..652b554
--- /dev/null
+++ b/tests/ClientManagement/New-PiHoleClient.Integration.Tests.ps1
@@ -0,0 +1,99 @@
+# Requires -Module Pester
+#
+# Integration tests that call a REAL Pi-hole server. Configure tests/IntegrationConfig.local.ps1
+# (copy it from IntegrationConfig.example.ps1) before running. Tests are skipped automatically
+# if that file is missing.
+
+$script:ConfigAvailable = Test-Path (Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1')
+
+Describe 'New-PiHoleClient (Integration)' -Tag 'Integration' {
+ BeforeAll {
+ Import-Module .\PiHoleShell\PiHoleShell.psm1 -Force
+
+ $script:TestClient = '192.168.99.99'
+ $script:TestGroupName = 'PesterClientGroup'
+
+ $configPath = Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1'
+ if (Test-Path $configPath) {
+ . $configPath
+ $script:PiHoleServer = $PiHoleServer
+ $script:PiHoleToken = $PiHoleToken
+ $script:PiHoleIgnoreSsl = $PiHoleIgnoreSsl
+
+ # Defensive cleanup in case a previous failed run left the test client/group behind
+ Remove-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient -Confirm:$false -ErrorAction SilentlyContinue -WarningAction SilentlyContinue | Out-Null
+ Remove-PiHoleGroup -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -GroupName $script:TestGroupName -ErrorAction SilentlyContinue -WarningAction SilentlyContinue | Out-Null
+
+ New-PiHoleGroup -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -GroupName $script:TestGroupName | Out-Null
+ }
+ }
+
+ AfterAll {
+ if ($script:PiHoleServer) {
+ # Ensures the test client/group are never left behind for other test files to trip over
+ Remove-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient -Confirm:$false -ErrorAction SilentlyContinue | Out-Null
+ Remove-PiHoleGroup -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -GroupName $script:TestGroupName -ErrorAction SilentlyContinue | Out-Null
+ }
+ }
+
+ It 'adds a new client and returns a formatted object' -Skip:(-not $script:ConfigAvailable) {
+ $result = New-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient -Comment 'Pester integration test client'
+ $result | Format-List | Out-String | Write-Host
+
+ $result | Should -Not -BeNullOrEmpty
+ $result.Client | Should -Be $script:TestClient
+ $result.Groups | Should -Contain 'Default'
+
+ # Clean up immediately so the next test starts from a known (client-absent) state
+ Remove-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient -Confirm:$false | Out-Null
+ }
+
+ It 'applies a non-default group passed via -Group' -Skip:(-not $script:ConfigAvailable) {
+ $result = New-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient -Group $script:TestGroupName
+ $result | Format-List | Out-String | Write-Host
+
+ $result | Should -Not -BeNullOrEmpty
+ $result.Groups | Should -Be @($script:TestGroupName)
+ $result.Groups | Should -Not -Contain 'Default'
+
+ Remove-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient -Confirm:$false | Out-Null
+ }
+
+ It 'errors when -Group names a group that does not exist, without creating the client' -Skip:(-not $script:ConfigAvailable) {
+ $result = New-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient -Group 'DefinitelyNotARealGroup' -ErrorVariable errOut -ErrorAction SilentlyContinue
+ Write-Host "Error ($($errOut.Count) entries, showing last): [$($errOut[-1])]"
+
+ $errOut | Should -Not -BeNullOrEmpty
+
+ $remaining = Get-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient
+ $remaining | Should -BeNullOrEmpty
+ }
+
+ It 'returns the raw API response when RawOutput is set' -Skip:(-not $script:ConfigAvailable) {
+ $result = New-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient -RawOutput $true
+ $result | Format-List | Out-String | Write-Host
+
+ $result | Should -Not -BeNullOrEmpty
+ $result.clients[0].client | Should -Be $script:TestClient
+
+ Remove-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient -Confirm:$false | Out-Null
+ }
+
+ It 'errors when the client already exists' -Skip:(-not $script:ConfigAvailable) {
+ New-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient | Out-Null
+
+ $result = New-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient -ErrorVariable errOut -ErrorAction SilentlyContinue
+ Write-Host "Error ($($errOut.Count) entries, showing last): [$($errOut[-1])]"
+
+ $errOut | Should -Not -BeNullOrEmpty
+
+ Remove-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient -Confirm:$false | Out-Null
+ }
+
+ It 'errors when given a bad password' -Skip:(-not $script:ConfigAvailable) {
+ $result = New-PiHoleClient -PiHoleServer $script:PiHoleServer -Password 'definitely-not-the-real-token' -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient -ErrorVariable errOut -ErrorAction SilentlyContinue
+ Write-Host "Error ($($errOut.Count) entries, showing last): [$($errOut[-1])]"
+
+ $errOut | Should -Not -BeNullOrEmpty
+ }
+}
diff --git a/tests/ClientManagement/Remove-PiHoleClient.Integration.Tests.ps1 b/tests/ClientManagement/Remove-PiHoleClient.Integration.Tests.ps1
new file mode 100644
index 0000000..a30919d
--- /dev/null
+++ b/tests/ClientManagement/Remove-PiHoleClient.Integration.Tests.ps1
@@ -0,0 +1,67 @@
+# Requires -Module Pester
+#
+# Integration tests that call a REAL Pi-hole server. Configure tests/IntegrationConfig.local.ps1
+# (copy it from IntegrationConfig.example.ps1) before running. Tests are skipped automatically
+# if that file is missing.
+
+$script:ConfigAvailable = Test-Path (Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1')
+
+Describe 'Remove-PiHoleClient (Integration)' -Tag 'Integration' {
+ BeforeAll {
+ Import-Module .\PiHoleShell\PiHoleShell.psm1 -Force
+
+ $script:TestClient = '192.168.99.99'
+
+ $configPath = Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1'
+ if (Test-Path $configPath) {
+ . $configPath
+ $script:PiHoleServer = $PiHoleServer
+ $script:PiHoleToken = $PiHoleToken
+ $script:PiHoleIgnoreSsl = $PiHoleIgnoreSsl
+
+ # Defensive cleanup in case a previous failed run left the test client behind
+ Remove-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient -Confirm:$false -ErrorAction SilentlyContinue -WarningAction SilentlyContinue | Out-Null
+ }
+ }
+
+ AfterAll {
+ if ($script:PiHoleServer) {
+ # Defensive cleanup in case a test left the client behind
+ Remove-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient -Confirm:$false -ErrorAction SilentlyContinue | Out-Null
+ }
+ }
+
+ It 'removes an existing client and returns a formatted object' -Skip:(-not $script:ConfigAvailable) {
+ New-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient -Comment 'Pester integration test client' | Out-Null
+
+ $result = Remove-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient -Confirm:$false
+ $result | Format-List | Out-String | Write-Host
+
+ $result | Should -Not -BeNullOrEmpty
+ $result.Client | Should -Be $script:TestClient
+ $result.Status | Should -Be 'Removed'
+
+ $remaining = Get-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient
+ $remaining | Should -BeNullOrEmpty
+ }
+
+ It 'removes the client when RawOutput is set, even though the API returns no body' -Skip:(-not $script:ConfigAvailable) {
+ New-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient | Out-Null
+
+ # A successful delete is HTTP 204 No Content, so RawOutput is expected to be empty here -
+ # the client actually being gone afterward is the real signal of success.
+ Remove-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient -RawOutput $true -Confirm:$false
+
+ $remaining = Get-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient
+ $remaining | Should -BeNullOrEmpty
+ }
+
+ It 'errors when given a bad password' -Skip:(-not $script:ConfigAvailable) {
+ New-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient | Out-Null
+
+ $result = Remove-PiHoleClient -PiHoleServer $script:PiHoleServer -Password 'definitely-not-the-real-token' -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient -Confirm:$false -ErrorVariable errOut -ErrorAction SilentlyContinue
+ Write-Host "Error ($($errOut.Count) entries, showing last): [$($errOut[-1])]"
+
+ $errOut | Should -Not -BeNullOrEmpty
+ }
+}
diff --git a/tests/ClientManagement/Update-PiHoleClient.Integration.Tests.ps1 b/tests/ClientManagement/Update-PiHoleClient.Integration.Tests.ps1
new file mode 100644
index 0000000..d3461de
--- /dev/null
+++ b/tests/ClientManagement/Update-PiHoleClient.Integration.Tests.ps1
@@ -0,0 +1,109 @@
+# Requires -Module Pester
+#
+# Integration tests that call a REAL Pi-hole server. Configure tests/IntegrationConfig.local.ps1
+# (copy it from IntegrationConfig.example.ps1) before running. Tests are skipped automatically
+# if that file is missing.
+
+$script:ConfigAvailable = Test-Path (Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1')
+
+Describe 'Update-PiHoleClient (Integration)' -Tag 'Integration' {
+ BeforeAll {
+ Import-Module .\PiHoleShell\PiHoleShell.psm1 -Force
+
+ $script:TestClient = '192.168.99.99'
+ $script:TestGroupName = 'PesterClientGroup'
+
+ $configPath = Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1'
+ if (Test-Path $configPath) {
+ . $configPath
+ $script:PiHoleServer = $PiHoleServer
+ $script:PiHoleToken = $PiHoleToken
+ $script:PiHoleIgnoreSsl = $PiHoleIgnoreSsl
+
+ # Defensive cleanup in case a previous failed run left the test client/group behind
+ Remove-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient -Confirm:$false -ErrorAction SilentlyContinue -WarningAction SilentlyContinue | Out-Null
+ Remove-PiHoleGroup -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -GroupName $script:TestGroupName -ErrorAction SilentlyContinue -WarningAction SilentlyContinue | Out-Null
+
+ New-PiHoleGroup -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -GroupName $script:TestGroupName | Out-Null
+ }
+ }
+
+ AfterAll {
+ if ($script:PiHoleServer) {
+ Remove-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient -Confirm:$false -ErrorAction SilentlyContinue | Out-Null
+ Remove-PiHoleGroup -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -GroupName $script:TestGroupName -ErrorAction SilentlyContinue | Out-Null
+ }
+ }
+
+ It 'updates only the comment, preserving Group' -Skip:(-not $script:ConfigAvailable) {
+ New-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient -Comment 'original comment' | Out-Null
+
+ $result = Update-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient -Comment 'updated comment'
+ $result | Format-List | Out-String | Write-Host
+
+ $result | Should -Not -BeNullOrEmpty
+ $result.Comment | Should -Be 'updated comment'
+ $result.Groups | Should -Contain 'Default'
+
+ Remove-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient -Confirm:$false | Out-Null
+ }
+
+ It 'updates only Group, preserving the current comment' -Skip:(-not $script:ConfigAvailable) {
+ New-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient -Comment 'keep this comment' | Out-Null
+
+ $result = Update-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient -Group $script:TestGroupName
+ $result | Format-List | Out-String | Write-Host
+
+ $result | Should -Not -BeNullOrEmpty
+ $result.Comment | Should -Be 'keep this comment'
+ $result.Groups | Should -Be @($script:TestGroupName)
+
+ Remove-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient -Confirm:$false | Out-Null
+ }
+
+ It 'errors when -Group names a group that does not exist' -Skip:(-not $script:ConfigAvailable) {
+ New-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient | Out-Null
+
+ $result = Update-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient -Group 'DefinitelyNotARealGroup' -ErrorVariable errOut -ErrorAction SilentlyContinue
+ Write-Host "Error ($($errOut.Count) entries, showing last): [$($errOut[-1])]"
+
+ $errOut | Should -Not -BeNullOrEmpty
+
+ Remove-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient -Confirm:$false | Out-Null
+ }
+
+ It 'returns the raw API response when RawOutput is set' -Skip:(-not $script:ConfigAvailable) {
+ New-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient | Out-Null
+
+ $result = Update-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient -Comment 'raw output test' -RawOutput $true
+ $result | Format-List | Out-String | Write-Host
+
+ $result.clients[0].comment | Should -Be 'raw output test'
+
+ Remove-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient -Confirm:$false | Out-Null
+ }
+
+ It 'errors when neither Comment nor Group is specified' -Skip:(-not $script:ConfigAvailable) {
+ New-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient | Out-Null
+
+ $result = Update-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient -ErrorVariable errOut -ErrorAction SilentlyContinue
+
+ $errOut | Should -Not -BeNullOrEmpty
+
+ Remove-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient -Confirm:$false | Out-Null
+ }
+
+ It 'errors when the client does not exist' -Skip:(-not $script:ConfigAvailable) {
+ $result = Update-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client '10.10.10.10' -Comment 'irrelevant' -ErrorVariable errOut -ErrorAction SilentlyContinue
+
+ $errOut | Should -Not -BeNullOrEmpty
+ }
+
+ It 'errors when given a bad password' -Skip:(-not $script:ConfigAvailable) {
+ New-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient | Out-Null
+
+ $result = Update-PiHoleClient -PiHoleServer $script:PiHoleServer -Password 'definitely-not-the-real-token' -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient -Comment 'irrelevant' -ErrorVariable errOut -ErrorAction SilentlyContinue
+
+ $errOut | Should -Not -BeNullOrEmpty
+ }
+}
diff --git a/tests/NetworkInformation/Get-PiHoleNetworkDevice.Integration.Tests.ps1 b/tests/NetworkInformation/Get-PiHoleNetworkDevice.Integration.Tests.ps1
new file mode 100644
index 0000000..3b96ca1
--- /dev/null
+++ b/tests/NetworkInformation/Get-PiHoleNetworkDevice.Integration.Tests.ps1
@@ -0,0 +1,44 @@
+# Requires -Module Pester
+#
+# Integration tests that call a REAL Pi-hole server. Configure tests/IntegrationConfig.local.ps1
+# (copy it from IntegrationConfig.example.ps1) before running. Tests are skipped automatically
+# if that file is missing.
+
+$script:ConfigAvailable = Test-Path (Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1')
+
+Describe 'Get-PiHoleNetworkDevice (Integration)' -Tag 'Integration' {
+ BeforeAll {
+ Import-Module .\PiHoleShell\PiHoleShell.psm1 -Force
+
+ $configPath = Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1'
+ if (Test-Path $configPath) {
+ . $configPath
+ $script:PiHoleServer = $PiHoleServer
+ $script:PiHoleToken = $PiHoleToken
+ $script:PiHoleIgnoreSsl = $PiHoleIgnoreSsl
+ }
+ }
+
+ # The test server may not have tracked any devices recently, so this only confirms the call
+ # succeeds rather than asserting specific devices are present.
+ It 'succeeds and returns without throwing, even with no devices tracked' -Skip:(-not $script:ConfigAvailable) {
+ { Get-PiHoleNetworkDevice -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl } | Should -Not -Throw
+ }
+
+ It 'accepts -MaxDevices and -MaxAddresses without error' -Skip:(-not $script:ConfigAvailable) {
+ { Get-PiHoleNetworkDevice -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -MaxDevices 5 -MaxAddresses 2 } | Should -Not -Throw
+ }
+
+ It 'returns the raw API response when RawOutput is set' -Skip:(-not $script:ConfigAvailable) {
+ $result = Get-PiHoleNetworkDevice -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -RawOutput $true
+ $result | Format-List | Out-String | Write-Host
+
+ $result | Should -Not -BeNullOrEmpty
+ }
+
+ It 'errors when given a bad password' -Skip:(-not $script:ConfigAvailable) {
+ $result = Get-PiHoleNetworkDevice -PiHoleServer $script:PiHoleServer -Password 'definitely-not-the-real-token' -IgnoreSsl $script:PiHoleIgnoreSsl -ErrorVariable errOut -ErrorAction SilentlyContinue
+
+ $errOut | Should -Not -BeNullOrEmpty
+ }
+}
diff --git a/tests/NetworkInformation/Get-PiHoleNetworkGateway.Integration.Tests.ps1 b/tests/NetworkInformation/Get-PiHoleNetworkGateway.Integration.Tests.ps1
new file mode 100644
index 0000000..e5118e6
--- /dev/null
+++ b/tests/NetworkInformation/Get-PiHoleNetworkGateway.Integration.Tests.ps1
@@ -0,0 +1,45 @@
+# Requires -Module Pester
+#
+# Integration tests that call a REAL Pi-hole server. Configure tests/IntegrationConfig.local.ps1
+# (copy it from IntegrationConfig.example.ps1) before running. Tests are skipped automatically
+# if that file is missing.
+
+$script:ConfigAvailable = Test-Path (Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1')
+
+Describe 'Get-PiHoleNetworkGateway (Integration)' -Tag 'Integration' {
+ BeforeAll {
+ Import-Module .\PiHoleShell\PiHoleShell.psm1 -Force
+
+ $configPath = Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1'
+ if (Test-Path $configPath) {
+ . $configPath
+ $script:PiHoleServer = $PiHoleServer
+ $script:PiHoleToken = $PiHoleToken
+ $script:PiHoleIgnoreSsl = $PiHoleIgnoreSsl
+ }
+ }
+
+ It 'returns gateway info as formatted objects' -Skip:(-not $script:ConfigAvailable) {
+ $result = Get-PiHoleNetworkGateway -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl
+ $result | Format-List | Out-String | Write-Host
+
+ $result | Should -Not -BeNullOrEmpty
+ }
+
+ It 'accepts -Detailed without error' -Skip:(-not $script:ConfigAvailable) {
+ { Get-PiHoleNetworkGateway -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Detailed $true } | Should -Not -Throw
+ }
+
+ It 'returns the raw API response when RawOutput is set' -Skip:(-not $script:ConfigAvailable) {
+ $result = Get-PiHoleNetworkGateway -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -RawOutput $true
+ $result | Format-List | Out-String | Write-Host
+
+ $result | Should -Not -BeNullOrEmpty
+ }
+
+ It 'errors when given a bad password' -Skip:(-not $script:ConfigAvailable) {
+ $result = Get-PiHoleNetworkGateway -PiHoleServer $script:PiHoleServer -Password 'definitely-not-the-real-token' -IgnoreSsl $script:PiHoleIgnoreSsl -ErrorVariable errOut -ErrorAction SilentlyContinue
+
+ $errOut | Should -Not -BeNullOrEmpty
+ }
+}
diff --git a/tests/NetworkInformation/Get-PiHoleNetworkInterface.Integration.Tests.ps1 b/tests/NetworkInformation/Get-PiHoleNetworkInterface.Integration.Tests.ps1
new file mode 100644
index 0000000..65f66e6
--- /dev/null
+++ b/tests/NetworkInformation/Get-PiHoleNetworkInterface.Integration.Tests.ps1
@@ -0,0 +1,45 @@
+# Requires -Module Pester
+#
+# Integration tests that call a REAL Pi-hole server. Configure tests/IntegrationConfig.local.ps1
+# (copy it from IntegrationConfig.example.ps1) before running. Tests are skipped automatically
+# if that file is missing.
+
+$script:ConfigAvailable = Test-Path (Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1')
+
+Describe 'Get-PiHoleNetworkInterface (Integration)' -Tag 'Integration' {
+ BeforeAll {
+ Import-Module .\PiHoleShell\PiHoleShell.psm1 -Force
+
+ $configPath = Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1'
+ if (Test-Path $configPath) {
+ . $configPath
+ $script:PiHoleServer = $PiHoleServer
+ $script:PiHoleToken = $PiHoleToken
+ $script:PiHoleIgnoreSsl = $PiHoleIgnoreSsl
+ }
+ }
+
+ It 'returns interfaces as formatted objects' -Skip:(-not $script:ConfigAvailable) {
+ $result = Get-PiHoleNetworkInterface -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl
+ $result | Select-Object -First 5 | Format-Table | Out-String | Write-Host
+
+ $result | Should -Not -BeNullOrEmpty
+ }
+
+ It 'accepts -Detailed without error' -Skip:(-not $script:ConfigAvailable) {
+ { Get-PiHoleNetworkInterface -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Detailed $true } | Should -Not -Throw
+ }
+
+ It 'returns the raw API response when RawOutput is set' -Skip:(-not $script:ConfigAvailable) {
+ $result = Get-PiHoleNetworkInterface -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -RawOutput $true
+ $result | Format-List | Out-String | Write-Host
+
+ $result | Should -Not -BeNullOrEmpty
+ }
+
+ It 'errors when given a bad password' -Skip:(-not $script:ConfigAvailable) {
+ $result = Get-PiHoleNetworkInterface -PiHoleServer $script:PiHoleServer -Password 'definitely-not-the-real-token' -IgnoreSsl $script:PiHoleIgnoreSsl -ErrorVariable errOut -ErrorAction SilentlyContinue
+
+ $errOut | Should -Not -BeNullOrEmpty
+ }
+}
diff --git a/tests/NetworkInformation/Get-PiHoleNetworkRoute.Integration.Tests.ps1 b/tests/NetworkInformation/Get-PiHoleNetworkRoute.Integration.Tests.ps1
new file mode 100644
index 0000000..9e7465a
--- /dev/null
+++ b/tests/NetworkInformation/Get-PiHoleNetworkRoute.Integration.Tests.ps1
@@ -0,0 +1,45 @@
+# Requires -Module Pester
+#
+# Integration tests that call a REAL Pi-hole server. Configure tests/IntegrationConfig.local.ps1
+# (copy it from IntegrationConfig.example.ps1) before running. Tests are skipped automatically
+# if that file is missing.
+
+$script:ConfigAvailable = Test-Path (Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1')
+
+Describe 'Get-PiHoleNetworkRoute (Integration)' -Tag 'Integration' {
+ BeforeAll {
+ Import-Module .\PiHoleShell\PiHoleShell.psm1 -Force
+
+ $configPath = Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1'
+ if (Test-Path $configPath) {
+ . $configPath
+ $script:PiHoleServer = $PiHoleServer
+ $script:PiHoleToken = $PiHoleToken
+ $script:PiHoleIgnoreSsl = $PiHoleIgnoreSsl
+ }
+ }
+
+ It 'returns routes as formatted objects' -Skip:(-not $script:ConfigAvailable) {
+ $result = Get-PiHoleNetworkRoute -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl
+ $result | Select-Object -First 5 | Format-Table | Out-String | Write-Host
+
+ $result | Should -Not -BeNullOrEmpty
+ }
+
+ It 'accepts -Detailed without error' -Skip:(-not $script:ConfigAvailable) {
+ { Get-PiHoleNetworkRoute -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Detailed $true } | Should -Not -Throw
+ }
+
+ It 'returns the raw API response when RawOutput is set' -Skip:(-not $script:ConfigAvailable) {
+ $result = Get-PiHoleNetworkRoute -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -RawOutput $true
+ $result | Format-List | Out-String | Write-Host
+
+ $result | Should -Not -BeNullOrEmpty
+ }
+
+ It 'errors when given a bad password' -Skip:(-not $script:ConfigAvailable) {
+ $result = Get-PiHoleNetworkRoute -PiHoleServer $script:PiHoleServer -Password 'definitely-not-the-real-token' -IgnoreSsl $script:PiHoleIgnoreSsl -ErrorVariable errOut -ErrorAction SilentlyContinue
+
+ $errOut | Should -Not -BeNullOrEmpty
+ }
+}
diff --git a/tests/NetworkInformation/Remove-PiHoleNetworkDevice.Integration.Tests.ps1 b/tests/NetworkInformation/Remove-PiHoleNetworkDevice.Integration.Tests.ps1
new file mode 100644
index 0000000..0bae3d4
--- /dev/null
+++ b/tests/NetworkInformation/Remove-PiHoleNetworkDevice.Integration.Tests.ps1
@@ -0,0 +1,38 @@
+# Requires -Module Pester
+#
+# Integration tests that call a REAL Pi-hole server. Configure tests/IntegrationConfig.local.ps1
+# (copy it from IntegrationConfig.example.ps1) before running. Tests are skipped automatically
+# if that file is missing.
+#
+# NOTE: Network devices arise from Pi-hole having genuinely seen a device on the network and
+# can't be manufactured on demand, so there's no reliable way to test a genuine successful
+# delete here. These tests instead verify the error paths, which still exercise the real
+# request/auth flow.
+
+$script:ConfigAvailable = Test-Path (Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1')
+
+Describe 'Remove-PiHoleNetworkDevice (Integration)' -Tag 'Integration' {
+ BeforeAll {
+ Import-Module .\PiHoleShell\PiHoleShell.psm1 -Force
+
+ $configPath = Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1'
+ if (Test-Path $configPath) {
+ . $configPath
+ $script:PiHoleServer = $PiHoleServer
+ $script:PiHoleToken = $PiHoleToken
+ $script:PiHoleIgnoreSsl = $PiHoleIgnoreSsl
+ }
+ }
+
+ It 'errors when the device does not exist' -Skip:(-not $script:ConfigAvailable) {
+ $result = Remove-PiHoleNetworkDevice -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -DeviceId 999999 -Confirm:$false -ErrorVariable errOut -ErrorAction SilentlyContinue
+
+ $errOut | Should -Not -BeNullOrEmpty
+ }
+
+ It 'errors when given a bad password' -Skip:(-not $script:ConfigAvailable) {
+ $result = Remove-PiHoleNetworkDevice -PiHoleServer $script:PiHoleServer -Password 'definitely-not-the-real-token' -IgnoreSsl $script:PiHoleIgnoreSsl -DeviceId 1 -Confirm:$false -ErrorVariable errOut -ErrorAction SilentlyContinue
+
+ $errOut | Should -Not -BeNullOrEmpty
+ }
+}
diff --git a/tools/Update-ExampleOutput.ps1 b/tools/Update-ExampleOutput.ps1
index ad26ca4..0f55591 100644
--- a/tools/Update-ExampleOutput.ps1
+++ b/tools/Update-ExampleOutput.ps1
@@ -52,6 +52,7 @@ $categoryOrder = [ordered]@{
GroupManagement = 'Group Management'
ListManagement = 'List Management'
DomainManagement = 'Domain Management'
+ ClientManagement = 'Client Management'
Metrics = 'Metrics'
Config = 'Configuration & Diagnostics'
Authentication = 'Authentication'
@@ -284,6 +285,35 @@ Add-Example -Category DomainManagement -FunctionName 'Remove-PiHoleDomain' `
-Result (Remove-PiHoleDomain -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl -Domain $docsDomain -Type Allow -Kind Exact -Confirm:$false)
#endregion
+#region ClientManagement
+$docsClient = '192.168.99.99'
+Invoke-Quietly { Remove-PiHoleClient -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl -Client $docsClient -Confirm:$false }
+
+Add-Example -Category ClientManagement -FunctionName 'New-PiHoleClient' `
+ -Invocation "New-PiHoleClient -PiHoleServer `$PiHoleServer -Password `$Password -Client `"$docsClient`" -Comment `"Example client`"" `
+ -Result (New-PiHoleClient -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl -Client $docsClient -Comment 'Example client')
+
+# Same settle-time reasoning as ListManagement/DomainManagement above - give the test server a
+# moment before relying on the client just being created.
+Start-Sleep -Seconds 3
+
+Add-Example -Category ClientManagement -FunctionName 'Get-PiHoleClient' `
+ -Invocation 'Get-PiHoleClient -PiHoleServer $PiHoleServer -Password $Password' `
+ -Result (Get-PiHoleClient -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl)
+
+Add-Example -Category ClientManagement -FunctionName 'Get-PiHoleClientSuggestion' `
+ -Invocation 'Get-PiHoleClientSuggestion -PiHoleServer $PiHoleServer -Password $Password' `
+ -Result (Get-PiHoleClientSuggestion -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl)
+
+Add-Example -Category ClientManagement -FunctionName 'Update-PiHoleClient' `
+ -Invocation "Update-PiHoleClient -PiHoleServer `$PiHoleServer -Password `$Password -Client `"$docsClient`" -Comment `"Updated comment`"" `
+ -Result (Update-PiHoleClient -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl -Client $docsClient -Comment 'Updated comment')
+
+Add-Example -Category ClientManagement -FunctionName 'Remove-PiHoleClient' `
+ -Invocation "Remove-PiHoleClient -PiHoleServer `$PiHoleServer -Password `$Password -Client `"$docsClient`"" `
+ -Result (Remove-PiHoleClient -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl -Client $docsClient -Confirm:$false)
+#endregion
+
#region Metrics
foreach ($fn in 'Get-PiHoleStatsSummary', 'Get-PiHoleStatsRecentBlocked', 'Get-PiHoleStatsQueryType', 'Get-PiHoleStatsTopDomain', 'Get-PiHoleStatsTopClient', 'Get-PiHoleStatsUpstream', 'Get-PiHoleStatsQuerySuggestions') {
Add-Example -Category Metrics -FunctionName $fn `
@@ -315,6 +345,34 @@ foreach ($fn in 'Get-PiHoleHistoryDatabase', 'Get-PiHoleHistoryDatabaseClient')
}
#endregion
+#region NetworkInformation
+# The NetworkInformation folder isn't its own README category either - same fallback-folding
+# reasoning as History above.
+Add-Example -Category Config -FunctionName 'Get-PiHoleNetworkGateway' `
+ -Invocation 'Get-PiHoleNetworkGateway -PiHoleServer $PiHoleServer -Password $Password' `
+ -Result (Get-PiHoleNetworkGateway -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl)
+
+Add-Example -Category Config -FunctionName 'Get-PiHoleNetworkRoute' `
+ -Invocation 'Get-PiHoleNetworkRoute -PiHoleServer $PiHoleServer -Password $Password' `
+ -Result (Get-PiHoleNetworkRoute -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl)
+
+Add-Example -Category Config -FunctionName 'Get-PiHoleNetworkInterface' `
+ -Invocation 'Get-PiHoleNetworkInterface -PiHoleServer $PiHoleServer -Password $Password' `
+ -Result (Get-PiHoleNetworkInterface -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl)
+
+Add-Example -Category Config -FunctionName 'Get-PiHoleNetworkDevice' `
+ -Invocation 'Get-PiHoleNetworkDevice -PiHoleServer $PiHoleServer -Password $Password' `
+ -Note 'Shows up to 10 devices by default; pass -MaxDevices for more.' `
+ -Result (Get-PiHoleNetworkDevice -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl)
+
+$dummyDeviceId = 999999
+Remove-PiHoleNetworkDevice -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl -DeviceId $dummyDeviceId -Confirm:$false -ErrorVariable removeDeviceError -ErrorAction SilentlyContinue | Out-Null
+Add-Example -Category Config -FunctionName 'Remove-PiHoleNetworkDevice' `
+ -Invocation 'Remove-PiHoleNetworkDevice -PiHoleServer $PiHoleServer -Password $Password -DeviceId 5' `
+ -Note 'Network devices arise from Pi-hole having genuinely seen a device and cannot be manufactured on demand, so this example shows the error for a device ID that does not exist rather than a fabricated success.' `
+ -Result $(if ($removeDeviceError) { [PSCustomObject]@{ Error = $removeDeviceError[-1].Exception.Message } })
+#endregion
+
#region Actions
if ($IncludeDisruptive) {
Add-Example -Category Actions -FunctionName 'Invoke-PiHoleFlushNetwork' `
diff --git a/tools/Update-ReadmeCommandReference.ps1 b/tools/Update-ReadmeCommandReference.ps1
index b31b5c0..9ea8337 100644
--- a/tools/Update-ReadmeCommandReference.ps1
+++ b/tools/Update-ReadmeCommandReference.ps1
@@ -24,6 +24,7 @@ $displayNames = [ordered]@{
GroupManagement = 'Group Management'
ListManagement = 'List Management'
DomainManagement = 'Domain Management'
+ ClientManagement = 'Client Management'
Metrics = 'Metrics'
Config = 'Configuration & Diagnostics'
Padd = 'Configuration & Diagnostics'
@@ -31,7 +32,7 @@ $displayNames = [ordered]@{
Teleporter = 'Configuration & Diagnostics'
Authentication = 'Authentication'
}
-$categoryOrder = @('Actions', 'DnsControl', 'GroupManagement', 'ListManagement', 'DomainManagement', 'Metrics', 'Config', 'Authentication')
+$categoryOrder = @('Actions', 'DnsControl', 'GroupManagement', 'ListManagement', 'DomainManagement', 'ClientManagement', 'Metrics', 'Config', 'Authentication')
$categoryIntros = @{
Authentication = 'Session handling is automatic for every command above, but these are available for managing sessions directly:'
}