From be8eccd08f0a296a0cd5ca53d34345bc6a4f3599 Mon Sep 17 00:00:00 2001 From: Mike Madeja Date: Sun, 27 Sep 2026 14:31:12 -0500 Subject: [PATCH 1/2] feat: add Config write/property functions, completing the Config API area Extends Get-PiHoleConfig with -Element (request one subset of the config tree, e.g. "dns/upstreams") and -Detailed. Adds Set-PiHoleConfig (PATCH /config), Add/Remove-PiHoleConfigArrayItem (PUT/DELETE /config/{element}/{value}, for array-type settings like dns/hosts and dns/cnameRecords), and Get-PiHoleConfigProperty (the list of settings that can never be changed via the API). All three write functions require the app password's "app_sudo" flag to be enabled in Pi-hole (Settings > All Settings) - Pi-hole blocks config changes from app passwords by default, and there's no way to enable it via the API itself (avoiding that would defeat the point). Verified against a real server with app_sudo both disabled and enabled: a real config value can be changed and reverted, an array item can be added and removed, and a genuinely read-only property (misc.readOnly) is correctly rejected. Adds ConvertTo-PiHoleFriendlyErrorMessage (Private/Misc.ps1), used by the three write functions' error handling: Pi-hole's own error responses carry a clearer message/hint than the generic HTTP exception text, and for the app_sudo case specifically, a concrete pointer to where to enable it - previously the module surfaced only "403 (Forbidden)" or "400 (Bad Request)" with no explanation. Co-Authored-By: Claude Sonnet 5 --- PiHoleShell/PiHoleShell.psm1 | 2 +- PiHoleShell/Private/Misc.ps1 | 38 ++ .../Config/Add-PiHoleConfigArrayItem.ps1 | 101 +++++ .../Public/Config/Get-PiHoleConfig.ps1 | 26 +- .../Config/Get-PiHoleConfigProperty.ps1 | 73 ++++ .../Config/Remove-PiHoleConfigArrayItem.ps1 | 103 +++++ .../Public/Config/Set-PiHoleConfig.ps1 | 97 +++++ README.md | 4 + docs/EXAMPLES.md | 365 +++++++++++------- ...iHoleConfigArrayItem.Integration.Tests.ps1 | 68 ++++ .../Get-PiHoleConfig.Integration.Tests.ps1 | 9 + ...PiHoleConfigProperty.Integration.Tests.ps1 | 43 +++ ...iHoleConfigArrayItem.Integration.Tests.ps1 | 76 ++++ .../Set-PiHoleConfig.Integration.Tests.ps1 | 69 ++++ tools/Update-ExampleOutput.ps1 | 42 ++ 15 files changed, 969 insertions(+), 147 deletions(-) create mode 100644 PiHoleShell/Public/Config/Add-PiHoleConfigArrayItem.ps1 create mode 100644 PiHoleShell/Public/Config/Get-PiHoleConfigProperty.ps1 create mode 100644 PiHoleShell/Public/Config/Remove-PiHoleConfigArrayItem.ps1 create mode 100644 PiHoleShell/Public/Config/Set-PiHoleConfig.ps1 create mode 100644 tests/Config/Add-PiHoleConfigArrayItem.Integration.Tests.ps1 create mode 100644 tests/Config/Get-PiHoleConfigProperty.Integration.Tests.ps1 create mode 100644 tests/Config/Remove-PiHoleConfigArrayItem.Integration.Tests.ps1 create mode 100644 tests/Config/Set-PiHoleConfig.Integration.Tests.ps1 diff --git a/PiHoleShell/PiHoleShell.psm1 b/PiHoleShell/PiHoleShell.psm1 index 59967d5..9524952 100644 --- a/PiHoleShell/PiHoleShell.psm1 +++ b/PiHoleShell/PiHoleShell.psm1 @@ -24,7 +24,7 @@ Export-ModuleMember -Function @( #DnsControl 'Get-PiHoleDnsBlockingStatus', 'Set-PiHoleDnsBlocking', ` #Config - 'Get-PiHoleConfig', ` + 'Get-PiHoleConfig', 'Set-PiHoleConfig', 'Add-PiHoleConfigArrayItem', 'Remove-PiHoleConfigArrayItem', 'Get-PiHoleConfigProperty', ` #Padd 'Get-PiHolePadd', ` #Metrics diff --git a/PiHoleShell/Private/Misc.ps1 b/PiHoleShell/Private/Misc.ps1 index a537078..3b0c02e 100644 --- a/PiHoleShell/Private/Misc.ps1 +++ b/PiHoleShell/Private/Misc.ps1 @@ -98,6 +98,44 @@ function ConvertTo-PiHolePascalCaseObject { } } +function ConvertTo-PiHoleFriendlyErrorMessage { + #INTERNAL FUNCTION + # + # Pi-hole's own error responses often carry a clearer message/hint than the generic HTTP + # exception text (e.g. "Unable to change configuration (read-only): ...app_sudo is false" + # vs just "403 Forbidden"). This extracts and combines them when present, and adds a + # concrete pointer to fix the most common cause of a blocked config write - the app + # password's app_sudo setting - since Pi-hole's own hint says what's wrong but not how to + # fix it. + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingEmptyCatchBlock", "", Justification = "Falls back to the raw exception message when the response body isn't valid JSON - nothing to report.")] + param ( + [Parameter(Mandatory = $true)] + $ErrorRecord + ) + + $Message = $ErrorRecord.Exception.Message + + if ($ErrorRecord.ErrorDetails.Message) { + try { + $ApiError = ($ErrorRecord.ErrorDetails.Message | ConvertFrom-Json).error + if ($ApiError.message) { + $Message = $ApiError.message + if ($ApiError.hint) { + $Message += ": $($ApiError.hint)" + } + if ($ApiError.hint -like '*app_sudo*') { + $Message += " Enable it in your Pi-hole admin UI under Settings > All Settings by searching for 'app_sudo' and setting webserver.api.app_sudo to true." + } + } + } + catch { + # ErrorDetails.Message wasn't valid JSON - fall back to the raw exception message + } + } + + return $Message +} + function Remove-PiHoleCurrentAuthSession { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "", Justification = "It removes sessions from PiHole only")] [CmdletBinding()] diff --git a/PiHoleShell/Public/Config/Add-PiHoleConfigArrayItem.ps1 b/PiHoleShell/Public/Config/Add-PiHoleConfigArrayItem.ps1 new file mode 100644 index 0000000..2d31bc9 --- /dev/null +++ b/PiHoleShell/Public/Config/Add-PiHoleConfigArrayItem.ps1 @@ -0,0 +1,101 @@ +function Add-PiHoleConfigArrayItem { + <# +.SYNOPSIS +Add config array item + +.DESCRIPTION +Adds one item to an array-type Pi-hole configuration setting - for example, an upstream DNS +server (dns/upstreams), a local DNS record (dns/hosts), or a CNAME record (dns/cnameRecords). +Use Set-PiHoleConfig instead for non-array settings. + +Requires your app password to have "app_sudo" enabled - Pi-hole blocks config changes from app +passwords by default. Enable it in your Pi-hole admin UI under Settings > All Settings by +searching for "app_sudo" and setting webserver.api.app_sudo to true. + +.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 Element +The array-type configuration setting to add to, as a slash-separated path (e.g. +"dns/upstreams", "dns/hosts", or "dns/cnameRecords") + +.PARAMETER Value +The item to add. For dns/hosts this is " "; for dns/cnameRecords this is +",[,]" + +.PARAMETER Restart +Whether to restart FTL immediately if this change requires it. Defaults to $true. Set to +$false to defer the restart, e.g. when adding several items independently rather than all at +once - you'll need to restart FTL manually later for the changes to take effect + +.PARAMETER IgnoreSsl +Set to $true to skip SSL certificate validation + +.PARAMETER RawOutput +This will dump the response instead of the formatted object + +.EXAMPLE +Add-PiHoleConfigArrayItem -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" -Element "dns/cnameRecords" -Value "alias.lan,target.lan" + #> + [CmdletBinding(HelpUri = 'https://ftl.pi-hole.net/master/docs/#put-/config/-element-/-value-')] + [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]$Element, + [Parameter(Mandatory = $true)] + [string]$Value, + [Nullable[bool]]$Restart, + [bool]$IgnoreSsl = $false, + [bool]$RawOutput = $false + ) + try { + $Sid = Request-PiHoleAuth -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl + + $Uri = "$($PiHoleServer.OriginalString)/api/config/$($Element.Trim('/'))/$([System.Uri]::EscapeDataString($Value))" + if ($PSBoundParameters.ContainsKey('Restart')) { + $Uri += "?restart=$($Restart.ToString().ToLower())" + } + + $Params = @{ + Headers = @{sid = $($Sid) } + Uri = $Uri + Method = "Put" + SkipCertificateCheck = $IgnoreSsl + ContentType = "application/json" + } + + $Response = Invoke-RestMethod @Params + + if ($RawOutput) { + Write-Output $Response + } + else { + # A successful add returns 201 Created with no body, so there's no response to + # build a rich object from. + $Object = [PSCustomObject]@{ + Element = $Element + Value = $Value + Status = "Added" + } + Write-Output $Object + } + } + + catch { + Write-Error -Message (ConvertTo-PiHoleFriendlyErrorMessage -ErrorRecord $_) + } + + finally { + if ($Sid) { + Remove-PiHoleCurrentAuthSession -PiHoleServer $PiHoleServer -Sid $Sid -IgnoreSsl $IgnoreSsl + } + } +} diff --git a/PiHoleShell/Public/Config/Get-PiHoleConfig.ps1 b/PiHoleShell/Public/Config/Get-PiHoleConfig.ps1 index 12cb171..f6fe735 100644 --- a/PiHoleShell/Public/Config/Get-PiHoleConfig.ps1 +++ b/PiHoleShell/Public/Config/Get-PiHoleConfig.ps1 @@ -7,7 +7,8 @@ Get current configuration of Pi-hole Request Pi-hole's full configuration tree (dns, dhcp, ntp, resolver, database, webserver, files, misc, and debug settings). The formatted output mirrors the API response as nested objects with PascalCase property names, so the entire configuration is available for -inspection rather than a hand-picked subset. +inspection rather than a hand-picked subset. Pass -Element to request just one subset of +the tree instead of everything. .PARAMETER PiHoleServer The URL to the PiHole Server, for example "http://pihole.domain.com:8080", or "http://192.168.1.100" @@ -15,6 +16,13 @@ The URL to the PiHole Server, for example "http://pihole.domain.com:8080", or "h .PARAMETER Password The API Password you generated from your PiHole server +.PARAMETER Element +Only return this part of the configuration tree, as a slash-separated path (e.g. +"dns/upstreams" or "dns/hosts"). Omit to return the entire configuration + +.PARAMETER Detailed +Include detailed information about the configuration (e.g. value types and validation info) + .PARAMETER IgnoreSsl Set to $true to skip SSL certificate validation @@ -26,6 +34,9 @@ Get-PiHoleConfig -PiHoleServer "http://pihole.domain.com:8080" -Password "your-a .EXAMPLE (Get-PiHoleConfig -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password").Dns.Upstreams + +.EXAMPLE +Get-PiHoleConfig -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" -Element "dns/upstreams" #> [CmdletBinding(HelpUri = 'https://ftl.pi-hole.net/master/docs/#get-/config')] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")] @@ -34,15 +45,26 @@ Get-PiHoleConfig -PiHoleServer "http://pihole.domain.com:8080" -Password "your-a [System.URI]$PiHoleServer, [Parameter(Mandatory = $true)] [string]$Password, + [string]$Element, + [Nullable[bool]]$Detailed, [bool]$IgnoreSsl = $false, [bool]$RawOutput = $false ) try { $Sid = Request-PiHoleAuth -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl + + $Uri = "$($PiHoleServer.OriginalString)/api/config" + if ($Element) { + $Uri += "/$($Element.Trim('/'))" + } + if ($PSBoundParameters.ContainsKey('Detailed')) { + $Uri += "?detailed=$($Detailed.ToString().ToLower())" + } + $Params = @{ Headers = @{sid = $($Sid) } - Uri = "$($PiHoleServer.OriginalString)/api/config" + Uri = $Uri Method = "Get" SkipCertificateCheck = $IgnoreSsl ContentType = "application/json" diff --git a/PiHoleShell/Public/Config/Get-PiHoleConfigProperty.ps1 b/PiHoleShell/Public/Config/Get-PiHoleConfigProperty.ps1 new file mode 100644 index 0000000..f9f6d50 --- /dev/null +++ b/PiHoleShell/Public/Config/Get-PiHoleConfigProperty.ps1 @@ -0,0 +1,73 @@ +function Get-PiHoleConfigProperty { + <# +.SYNOPSIS +Get special properties of your Pi-hole configuration + +.DESCRIPTION +Returns the configuration properties that cannot be changed through the API (e.g. because +they're only settable in pihole.toml, or are controlled by an environment variable), along with +why each one is restricted. + +.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-PiHoleConfigProperty -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" + #> + [CmdletBinding(HelpUri = 'https://ftl.pi-hole.net/master/docs/#get-/config/_properties')] + [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/config/_properties" + Method = "Get" + SkipCertificateCheck = $IgnoreSsl + ContentType = "application/json" + } + + $Response = Invoke-RestMethod @Params + + if ($RawOutput) { + Write-Output $Response + } + else { + $ObjectFinal = foreach ($Item in $Response.config.read_only) { + [PSCustomObject]@{ + Key = $Item.key + Reason = $Item.reason + Description = $Item.description + } + } + 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/Config/Remove-PiHoleConfigArrayItem.ps1 b/PiHoleShell/Public/Config/Remove-PiHoleConfigArrayItem.ps1 new file mode 100644 index 0000000..f197ea8 --- /dev/null +++ b/PiHoleShell/Public/Config/Remove-PiHoleConfigArrayItem.ps1 @@ -0,0 +1,103 @@ +function Remove-PiHoleConfigArrayItem { + <# +.SYNOPSIS +Delete config array item + +.DESCRIPTION +Removes one item from an array-type Pi-hole configuration setting - for example, an upstream +DNS server (dns/upstreams), a local DNS record (dns/hosts), or a CNAME record +(dns/cnameRecords). + +Requires your app password to have "app_sudo" enabled - Pi-hole blocks config changes from app +passwords by default. Enable it in your Pi-hole admin UI under Settings > All Settings by +searching for "app_sudo" and setting webserver.api.app_sudo to true. + +.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 Element +The array-type configuration setting to remove from, as a slash-separated path (e.g. +"dns/upstreams", "dns/hosts", or "dns/cnameRecords") + +.PARAMETER Value +The exact item to remove, as it appears in the array (see Get-PiHoleConfig) + +.PARAMETER Restart +Whether to restart FTL immediately if this change requires it. Defaults to $true. Set to +$false to defer the restart, e.g. when removing several items independently rather than all at +once - you'll need to restart FTL manually later for the changes to take effect + +.PARAMETER IgnoreSsl +Set to $true to skip SSL certificate validation + +.PARAMETER RawOutput +This will dump the response instead of the formatted object + +.EXAMPLE +Remove-PiHoleConfigArrayItem -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" -Element "dns/cnameRecords" -Value "alias.lan,target.lan" + #> + [CmdletBinding(SupportsShouldProcess = $true, HelpUri = 'https://ftl.pi-hole.net/master/docs/#delete-/config/-element-/-value-')] + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "Password")] + param ( + [Parameter(Mandatory = $true)] + [System.URI]$PiHoleServer, + [Parameter(Mandatory = $true)] + [string]$Password, + [Parameter(Mandatory = $true)] + [string]$Element, + [Parameter(Mandatory = $true)] + [string]$Value, + [Nullable[bool]]$Restart, + [bool]$IgnoreSsl = $false, + [bool]$RawOutput = $false + ) + try { + $Target = "Pi-Hole config item $Value in $Element" + if ($PSCmdlet.ShouldProcess($Target, "Remove config array item")) { + $Sid = Request-PiHoleAuth -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl + + $Uri = "$($PiHoleServer.OriginalString)/api/config/$($Element.Trim('/'))/$([System.Uri]::EscapeDataString($Value))" + if ($PSBoundParameters.ContainsKey('Restart')) { + $Uri += "?restart=$($Restart.ToString().ToLower())" + } + + $Params = @{ + Headers = @{sid = $($Sid) } + Uri = $Uri + 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]@{ + Element = $Element + Value = $Value + Status = "Removed" + } + Write-Output $Object + } + } + } + + catch { + Write-Error -Message (ConvertTo-PiHoleFriendlyErrorMessage -ErrorRecord $_) + } + + finally { + if ($Sid) { + Remove-PiHoleCurrentAuthSession -PiHoleServer $PiHoleServer -Sid $Sid -IgnoreSsl $IgnoreSsl + } + } +} diff --git a/PiHoleShell/Public/Config/Set-PiHoleConfig.ps1 b/PiHoleShell/Public/Config/Set-PiHoleConfig.ps1 new file mode 100644 index 0000000..2c4eaf0 --- /dev/null +++ b/PiHoleShell/Public/Config/Set-PiHoleConfig.ps1 @@ -0,0 +1,97 @@ +function Set-PiHoleConfig { + <# +.SYNOPSIS +Change configuration of your Pi-hole + +.DESCRIPTION +Updates one or more Pi-hole configuration settings in a single call. -Settings takes a +hashtable shaped like the configuration tree returned by Get-PiHoleConfig - only include the +properties you want to change; everything else is left untouched. Some changes require FTL to +restart to take effect; this happens automatically unless -Restart is set to $false. + +Requires your app password to have "app_sudo" enabled - Pi-hole blocks config changes from app +passwords by default. Enable it in your Pi-hole admin UI under Settings > All Settings by +searching for "app_sudo" and setting webserver.api.app_sudo to true. Some settings can never be +changed via the API at all (only in pihole.toml, or via an environment variable) - see +Get-PiHoleConfigProperty for the current list. + +.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 Settings +A hashtable of the configuration properties to change, shaped like the tree returned by +Get-PiHoleConfig (e.g. @{ dns = @{ CNAMEdeepInspect = $true } }) + +.PARAMETER Restart +Whether to restart FTL immediately if this change requires it. Defaults to $true. Set to +$false to defer the restart, e.g. when making several changes independently rather than all at +once - you'll need to restart FTL manually later for the changes to take effect + +.PARAMETER IgnoreSsl +Set to $true to skip SSL certificate validation + +.PARAMETER RawOutput +This will dump the response instead of the formatted object + +.EXAMPLE +Set-PiHoleConfig -PiHoleServer "http://pihole.domain.com:8080" -Password "your-app-password" -Settings @{ dns = @{ CNAMEdeepInspect = $true } } + #> + [CmdletBinding(HelpUri = 'https://ftl.pi-hole.net/master/docs/#patch-/config')] + [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)] + [hashtable]$Settings, + [Nullable[bool]]$Restart, + [bool]$IgnoreSsl = $false, + [bool]$RawOutput = $false + ) + try { + $Sid = Request-PiHoleAuth -PiHoleServer $PiHoleServer -Password $Password -IgnoreSsl $IgnoreSsl + + $Uri = "$($PiHoleServer.OriginalString)/api/config" + if ($PSBoundParameters.ContainsKey('Restart')) { + $Uri += "?restart=$($Restart.ToString().ToLower())" + } + + $Body = @{ + config = $Settings + } + + $Params = @{ + Headers = @{sid = $($Sid) } + Uri = $Uri + Method = "Patch" + SkipCertificateCheck = $IgnoreSsl + Body = $Body | ConvertTo-Json -Depth 10 + ContentType = "application/json" + } + + $Response = Invoke-RestMethod @Params + + if ($RawOutput) { + Write-Output $Response + } + else { + $Object = ConvertTo-PiHolePascalCaseObject -InputObject $Response.config + Write-Output $Object + } + } + + catch { + Write-Error -Message (ConvertTo-PiHoleFriendlyErrorMessage -ErrorRecord $_) + } + + finally { + if ($Sid) { + Remove-PiHoleCurrentAuthSession -PiHoleServer $PiHoleServer -Sid $Sid -IgnoreSsl $IgnoreSsl + } + } +} diff --git a/README.md b/README.md index 37671ed..ad4c0a5 100644 --- a/README.md +++ b/README.md @@ -151,7 +151,9 @@ See [docs/EXAMPLES.md](docs/EXAMPLES.md) for real, captured output from every fu | Function | Description | |---|---| +| `Add-PiHoleConfigArrayItem` | Add config array item | | `Get-PiHoleConfig` | Get current configuration of Pi-hole | +| `Get-PiHoleConfigProperty` | Get special properties of your Pi-hole configuration | | `Get-PiHoleHistory` | Get activity graph data | | `Get-PiHoleHistoryClient` | Get per-client activity graph data | | `Get-PiHoleHistoryDatabase` | Get activity graph data (long-term data) | @@ -170,7 +172,9 @@ See [docs/EXAMPLES.md](docs/EXAMPLES.md) for real, captured output from every fu | `Get-PiHoleLogWebserver` | Get webserver log content | | `Get-PiHolePadd` | Get summarized data for PADD | | `Get-PiHoleTeleporterDownload` | Export Pi-hole settings | +| `Remove-PiHoleConfigArrayItem` | Delete config array item | | `Remove-PiHoleInfoMessage` | Delete a Pi-hole diagnosis message | +| `Set-PiHoleConfig` | Change configuration of your Pi-hole | ### Authentication diff --git a/docs/EXAMPLES.md b/docs/EXAMPLES.md index a0deb07..9f97dcf 100644 --- a/docs/EXAMPLES.md +++ b/docs/EXAMPLES.md @@ -99,9 +99,9 @@ New-PiHoleGroup -PiHoleServer $PiHoleServer -Password $Password -GroupName "PiHo 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 +Id : 17 +DateAdded : 9/27/2026 2:18:02 PM +DateModified : 9/27/2026 2:18:02 PM ``` ### Get-PiHoleGroup @@ -136,9 +136,9 @@ DateModified : 7/1/2025 10:13:40 PM 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 +Id : 17 +DateAdded : 9/27/2026 2:18:02 PM +DateModified : 9/27/2026 2:18:02 PM ``` ### Update-PiHoleGroup @@ -152,9 +152,9 @@ Update-PiHoleGroup -PiHoleServer $PiHoleServer -Password $Password -GroupName "P 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 +Id : 17 +DateAdded : 9/27/2026 2:18:02 PM +DateModified : 9/27/2026 2:18:21 PM ``` ### Remove-PiHoleGroup @@ -183,9 +183,9 @@ Address : https://blocklistproject.github.io/Lists/alt-version/ransomware 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 +Id : 79 +DateAdded : 9/27/2026 2:18:57 PM +DateModified : 9/27/2026 2:18:57 PM 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 : @@ -287,9 +287,9 @@ Address : https://blocklistproject.github.io/Lists/alt-version/ransomware 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 +Id : 79 +DateAdded : 9/27/2026 2:18:57 PM +DateModified : 9/27/2026 2:19:31 PM 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 : 16 +DateAdded : 9/27/2026 2:20:09 PM +DateModified : 9/27/2026 2:20:09 PM ``` ### Get-PiHoleDomain @@ -434,9 +434,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 : 16 +DateAdded : 9/27/2026 2:20:09 PM +DateModified : 9/27/2026 2:20:09 PM ``` ### Update-PiHoleDomain @@ -454,9 +454,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 : 16 +DateAdded : 9/27/2026 2:20:09 PM +DateModified : 9/27/2026 2:20:42 PM ``` ### Remove-PiHoleDomain @@ -483,18 +483,18 @@ Get-PiHoleStatsSummary -PiHoleServer $PiHoleServer -Password $Password ``` -Total : 281 -Blocked : 84 -PercentBlocked : 29.893238067627 +Total : 265 +Blocked : 82 +PercentBlocked : 30.943395614624 UniqueDomains : 21 -Forwarded : 37 -Cached : 160 +Forwarded : 33 +Cached : 150 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} +Types : @{A=129; AAAA=130; ANY=0; SRV=0; SOA=0; PTR=6; TXT=0; NAPTR=0; MX=0; DS=0; RRSIG=0; DNSKEY=0; NS=0; SVCB=0; HTTPS=0; OTHER=0} +Status : @{Unknown=0; Gravity=82; Forwarded=33; Cache=123; 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=27; ExternalBlockedEde15=0} +Replies : @{Unknown=8; Nodata=20; Nxdomain=2; Cname=0; Ip=231; Domain=4; 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} +Gravity : @{DomainsBeingBlocked=496171; LastUpdate=1790530224} ``` ### Get-PiHoleStatsRecentBlocked @@ -505,7 +505,7 @@ Get-PiHoleStatsRecentBlocked -PiHoleServer $PiHoleServer -Password $Password ``` -Blocked : googleadservices.com +Blocked : adservice.google.com ``` ### Get-PiHoleStatsQueryType @@ -517,7 +517,7 @@ Get-PiHoleStatsQueryType -PiHoleServer $PiHoleServer -Password $Password ``` Type : A -Count : 130 +Count : 129 Type : AAAA Count : 130 @@ -542,19 +542,19 @@ Get-PiHoleStatsTopDomain -PiHoleServer $PiHoleServer -Password $Password ``` -Domain : wikipedia.org -Count : 20 +Domain : python.org +Count : 22 -Domain : bbc.com -Count : 18 +Domain : microsoft.com +Count : 20 -Domain : python.org +Domain : nytimes.com Count : 18 Domain : example.com Count : 18 -Domain : mozilla.org +Domain : wikipedia.org Count : 16 _(showing 5 of 10 results)_ @@ -570,11 +570,11 @@ Get-PiHoleStatsTopClient -PiHoleServer $PiHoleServer -Password $Password IP : 192.168.1.162 Name : -Count : 260 +Count : 259 IP : 127.0.0.1 Name : localhost -Count : 21 +Count : 6 ``` ### Get-PiHoleStatsUpstream @@ -585,9 +585,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 : 265 +ForwardedQueries : 33 +Upstreams : {@{Ip=blocklist; Name=blocklist; Port=-1; Count=82; ResponseTime=0; Variance=0}, @{Ip=cache; Name=cache; Port=-1; Count=150; ResponseTime=0; Variance=0}, @{Ip=8.8.8.8; Name=dns.google; Port=53; Count=33; ResponseTime=0.0300731555275295; Variance=0.0024513289105294}} ``` ### Get-PiHoleStatsQuerySuggestions @@ -598,7 +598,7 @@ Get-PiHoleStatsQuerySuggestions -PiHoleServer $PiHoleServer -Password $Password ``` -Domain : {wikipedia.org, bbc.com, python.org, example.com…} +Domain : {python.org, microsoft.com, nytimes.com, example.com…} ClientIp : {192.168.1.162, 127.0.0.1} ClientName : {localhost} Upstream : {blocklist, cache, 8.8.8.8#53 (dns.google), permitted} @@ -618,10 +618,10 @@ Get-PiHoleStatsDatabaseSummary -PiHoleServer $PiHoleServer -Password $Password ``` -SumQueries : 8 -SumBlocked : 0 -PercentBlocked : 0 -TotalClients : 1 +SumQueries : 265 +SumBlocked : 82 +PercentBlocked : 30.943395614624 +TotalClients : 3 ``` ### Get-PiHoleStatsDatabaseQueryType @@ -635,7 +635,7 @@ Get-PiHoleStatsDatabaseQueryType -PiHoleServer $PiHoleServer -Password $Password ``` Type : A -Count : 0 +Count : 130 Type : AAAA Count : 0 @@ -647,7 +647,7 @@ Type : SRV Count : 0 Type : SOA -Count : 8 +Count : 6 _(showing 5 of 16 results)_ ``` @@ -662,8 +662,22 @@ Get-PiHoleStatsDatabaseTopDomain -PiHoleServer $PiHoleServer -Password $Password ``` -Domain : 1.0.0.127.in-addr.arpa -Count : 8 +Domain : python.org +Count : 22 + +Domain : microsoft.com +Count : 20 + +Domain : nytimes.com +Count : 18 + +Domain : example.com +Count : 18 + +Domain : wikipedia.org +Count : 16 + +_(showing 5 of 10 results)_ ``` ### Get-PiHoleStatsDatabaseTopClient @@ -676,9 +690,17 @@ Get-PiHoleStatsDatabaseTopClient -PiHoleServer $PiHoleServer -Password $Password ``` +IP : 192.168.1.162 +Name : +Count : 177 + IP : 127.0.0.1 Name : localhost -Count : 8 +Count : 4 + +IP : 127.0.0.1 +Name : +Count : 2 ``` ### Get-PiHoleStatsDatabaseUpstream @@ -691,9 +713,9 @@ Get-PiHoleStatsDatabaseUpstream -PiHoleServer $PiHoleServer -Password $Password ``` -TotalQueries : 8 -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}} +TotalQueries : 265 +ForwardedQueries : 33 +Upstreams : {@{Ip=1; Name=; Port=4294967295; Count=1; ResponseTime=0; Variance=0}, @{Ip=2; Name=; Port=4294967295; Count=32; ResponseTime=0; Variance=0}, @{Ip=cache; Name=cache; Port=-1; Count=123; ResponseTime=0; Variance=0}, @{Ip=blocklist; Name=blocklist; Port=-1; Count=109; ResponseTime=0; Variance=0}} ``` ## Configuration & Diagnostics @@ -717,6 +739,64 @@ Misc : @{Privacylevel=0; DelayStartup=0; Nice=-10; Addr2line=True; EtcDnsma Debug : @{Database=False; Networking=False; Locks=False; Queries=False; Flags=False; Shmem=False; Gc=False; Arp=False; Regex=False; Api=False; Tls=False; Overtime=False; Status=False; Caps=False; Dnssec=False; Vectors=False; Resolver=False; Edns0=False; Clients=False; Aliasclients=False; Events=False; Helper=False; Config=False; Inotify=False; Webserver=False; Extra=False; Reserved=False; Ntp=False; Netlink=False; Timing=False; Performance=False; All=False} ``` +### Get-PiHoleConfigProperty + +```powershell +Get-PiHoleConfigProperty -PiHoleServer $PiHoleServer -Password $Password +``` + +``` + +Key : misc.readOnly +Reason : read_only +Description : Variable can only be set in pihole.toml, not via API +``` + +### Set-PiHoleConfig + +```powershell +Set-PiHoleConfig -PiHoleServer $PiHoleServer -Password $Password -Settings @{ debug = @{ api = $true } } +``` + +``` + +Dns : @{Upstreams=System.Object[]; CNAMEdeepInspect=True; BlockESNI=True; EDNS0ECS=True; IgnoreLocalhost=False; ShowDNSSEC=True; AnalyzeOnlyAandAAAA=False; PiholePTR=PI.HOLE; ReplyWhenBusy=ALLOW; BlockTTL=2; Hosts=System.Object[]; DomainNeeded=False; ExpandHosts=False; BogusPriv=True; Dnssec=False; Interface=wlan0; HostRecord=; ListeningMode=LOCAL; QueryLogging=False; CnameRecords=; Port=53; Localise=True; RevServers=; Domain=; Cache=; Blocking=; SpecialDomains=; Reply=; RateLimit=} +Dhcp : @{Active=False; Start=; End=; Router=; Netmask=; LeaseTime=; Ipv6=False; RapidCommit=False; MultiDNS=False; Logging=False; IgnoreUnknownClients=False; Hosts=} +Ntp : @{Ipv4=; Ipv6=; Sync=} +Resolver : @{ResolveIPv4=True; ResolveIPv6=True; MacNames=True; NetworkNames=True; RefreshNames=IPV4_ONLY} +Database : @{DBimport=True; MaxDBdays=2; DBinterval=60; UseWAL=True; ForceDisk=False; Network=} +Webserver : @{Domain=pi.hole; Acl=; Port=8089,8489s; Threads=50; Headers=System.Object[]; ServeAll=False; AdvancedOpts=; Session=; Tls=; Paths=; Interface=; Api=} +Files : @{Database=/etc/pihole/pihole-FTL.db; TmpDb=/etc/pihole/pihole-tmp.db; Gravity=/etc/pihole/gravity.db; GravityTmp=/tmp; Macvendor=/etc/pihole/macvendor.db; Pcap=; Log=} +Misc : @{Privacylevel=0; DelayStartup=0; Nice=-10; Addr2line=True; EtcDnsmasqD=False; DnsmasqLines=; ExtraLogging=False; ReadOnly=False; NormalizeCPU=True; HideDnsmasqWarn=False; HideConnectionError=False; Check=} +Debug : @{Database=False; Networking=False; Locks=False; Queries=False; Flags=False; Shmem=False; Gc=False; Arp=False; Regex=False; Api=True; Tls=False; Overtime=False; Status=False; Caps=False; Dnssec=False; Vectors=False; Resolver=False; Edns0=False; Clients=False; Aliasclients=False; Events=False; Helper=False; Config=False; Inotify=False; Webserver=False; Extra=False; Reserved=False; Ntp=False; Netlink=False; Timing=False; Performance=False; All=False} +``` + +### Add-PiHoleConfigArrayItem + +```powershell +Add-PiHoleConfigArrayItem -PiHoleServer $PiHoleServer -Password $Password -Element "dns/hosts" -Value "192.0.2.1 piholeshell-docs-example.com" +``` + +``` + +Element : dns/hosts +Value : 192.0.2.1 piholeshell-docs-example.com +Status : Added +``` + +### Remove-PiHoleConfigArrayItem + +```powershell +Remove-PiHoleConfigArrayItem -PiHoleServer $PiHoleServer -Password $Password -Element "dns/hosts" -Value "192.0.2.1 piholeshell-docs-example.com" +``` + +``` + +Element : dns/hosts +Value : 192.0.2.1 piholeshell-docs-example.com +Status : Removed +``` + ### Get-PiHolePadd ```powershell @@ -729,20 +809,20 @@ CpuPercent : 0 MemoryPercent : 0 ActiveClients : 2 Blocking : enabled -Cache : @{Size=10000; Inserted=96; Evicted=0} +Cache : @{Size=10000; Inserted=127; 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 +Queries : @{Total=265; Blocked=82; PercentBlocked=30.943395614624; QueryFrequency=0} +RecentBlocked : adservice.google.com +Sensors : @{CpuTemp=37.932; HotLimit=60; Unit=C} +System : @{Uptime=653422; Memory=; Procs=80; Cpu=; Ftl=} +TopBlocked : analytics.google.com TopClient : 192.168.1.162 -TopDomain : wikipedia.org +TopDomain : python.org Version : @{Core=; Web=; Ftl=; Docker=} ``` @@ -779,11 +859,11 @@ Get-PiHoleInfoSystem -PiHoleServer $PiHoleServer -Password $Password ``` -Uptime : 629884 +Uptime : 653433 Memory : @{Ram=; Swap=} -Procs : 70 -Cpu : @{NumProcessors=1; PercentCpu=96.8000030517578; Load=} -Ftl : @{PercentMemory=2.40430068969727; PercentCpu=96.3000030517578} +Procs : 81 +Cpu : @{NumProcessors=1; PercentCpu=96.1999969482422; Load=} +Ftl : @{PercentMemory=3.29122042655945; PercentCpu=95.3000030517578} ``` ### Get-PiHoleInfoFtl @@ -794,16 +874,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} Pid : 389 -Uptime : 51790377.711423 -PercentMemory : 2.40430068969727 -PercentCpu : 96.3000030517578 +Uptime : 6534699.497781 +PercentMemory : 3.29122042655945 +PercentCpu : 96.1999969482422 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=127; DnsCacheLiveFreed=0; DnsQueriesForwarded=60; DnsAuthAnswered=0; DnsLocalAnswered=205; DnsStaleAnswered=27; 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 +895,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 : 37.932 HotLimit : 60 Unit : C ``` @@ -832,12 +912,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 12:28:00 PM +ChangeTime : 9/27/2026 12:28:00 PM Owner : @{User=; Group=} -Queries : 281 -EarliestTimestamp : 9/26/2026 7:40:00 AM -QueriesDisk : 281 +Queries : 265 +EarliestTimestamp : 9/26/2026 2:10:00 PM +QueriesDisk : 265 EarliestTimestampDisk : SqliteVersion : 3.53.1 ``` @@ -905,7 +985,12 @@ Get-PiHoleInfoMessage -PiHoleServer $PiHoleServer -Password $Password ``` ``` -(no output) + +Id : 1 +Timestamp : 9/27/2026 1:13:02 PM +Type : LOAD +Plain : Long-term load (15min avg) larger than number of processors: 1.1 > 1 +Html : Long-term load (15min avg) larger than number of processors: 1.1 > 1
This may slow down DNS resolution and can cause bottlenecks. ``` ### Get-PiHoleInfoMessageCount @@ -916,7 +1001,7 @@ Get-PiHoleInfoMessageCount -PiHoleServer $PiHoleServer -Password $Password ``` -Count : 0 +Count : 1 ``` ### Remove-PiHoleInfoMessage @@ -940,8 +1025,8 @@ Get-PiHoleLogWebserver -PiHoleServer $PiHoleServer -Password $Password ``` -Log : @{Timestamp=9/26/2026 5:21:18 PM; Message=Initializing HTTP server on ports "8089,8489s"; Priority=} -NextID : 1 +Log : {@{Timestamp=9/27/2026 12:28:02 PM; Message=Initializing HTTP server on ports "8089,8489s"; Priority=}, @{Timestamp=9/27/2026 1:54:43 PM; Message=ACCESS: 192.168.1.162 - - [27/Sep/2026:18:54:42 +0000] "PATCH /api/config?restart=false HTTP/1.1" 200 4590 - Mozilla/5.0 (Windows NT 10.0; Microsoft Windows 10.0.26200; en-US) PowerShell/7.6.6; Priority=}, @{Timestamp=9/27/2026 1:54:44 PM; Message=ACCESS: 192.168.1.162 - - [27/Sep/2026:18:54:43 +0000] "DELETE /api/auth HTTP/1.1" 204 558 - Mozilla/5.0 (Windows NT 10.0; Microsoft Windows 10.0.26200; en-US) PowerShell/7.6.6; Priority=}, @{Timestamp=9/27/2026 1:54:48 PM; Message=ACCESS: 192.168.1.162 - - [27/Sep/2026:18:54:47 +0000] "GET /api/dns/blocking HTTP/1.1" 200 692 https://dns3.localdomain:8489/admin/settings/all Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.0.0 Safari/537.36...; Priority=}…} +NextID : 14 Pid : 389 File : /var/log/pihole/webserver.log ``` @@ -959,7 +1044,7 @@ Get-PiHoleTeleporterDownload -PiHoleServer $PiHoleServer -Password $Password -Fo FileName : pihole-backup.zip FilePath : C:\Users\mmadeja\AppData\Local\Temp\PiHoleShellDocsExample\pihole-backup.zip RootFolder : C:\Users\mmadeja\AppData\Local\Temp\PiHoleShellDocsExample -FileSizeKB : 24 +FileSizeKB : 25 ``` ### Get-PiHoleHistory @@ -970,31 +1055,31 @@ Get-PiHoleHistory -PiHoleServer $PiHoleServer -Password $Password ``` -Timestamp : 9/26/2026 7:55:00 AM +Timestamp : 9/26/2026 2:25:00 PM Total : 0 Cached : 0 Blocked : 0 Forwarded : 0 -Timestamp : 9/26/2026 8:05:00 AM +Timestamp : 9/26/2026 2:35:00 PM Total : 0 Cached : 0 Blocked : 0 Forwarded : 0 -Timestamp : 9/26/2026 8:15:00 AM +Timestamp : 9/26/2026 2:45:00 PM Total : 0 Cached : 0 Blocked : 0 Forwarded : 0 -Timestamp : 9/26/2026 8:25:00 AM +Timestamp : 9/26/2026 2:55:00 PM Total : 0 Cached : 0 Blocked : 0 Forwarded : 0 -Timestamp : 9/26/2026 8:35:00 AM +Timestamp : 9/26/2026 3:05:00 PM Total : 0 Cached : 0 Blocked : 0 @@ -1011,19 +1096,19 @@ Get-PiHoleHistoryClient -PiHoleServer $PiHoleServer -Password $Password ``` -Timestamp : 9/26/2026 7:55:00 AM +Timestamp : 9/26/2026 2:25:00 PM Clients : {@{IP=192.168.1.162; Name=; Count=0}, @{IP=others; Name=; Count=0}} -Timestamp : 9/26/2026 8:05:00 AM +Timestamp : 9/26/2026 2:35:00 PM Clients : {@{IP=192.168.1.162; Name=; Count=0}, @{IP=others; Name=; Count=0}} -Timestamp : 9/26/2026 8:15:00 AM +Timestamp : 9/26/2026 2:45:00 PM Clients : {@{IP=192.168.1.162; Name=; Count=0}, @{IP=others; Name=; Count=0}} -Timestamp : 9/26/2026 8:25:00 AM +Timestamp : 9/26/2026 2:55:00 PM Clients : {@{IP=192.168.1.162; Name=; Count=0}, @{IP=others; Name=; Count=0}} -Timestamp : 9/26/2026 8:35:00 AM +Timestamp : 9/26/2026 3:05:00 PM Clients : {@{IP=192.168.1.162; Name=; Count=0}, @{IP=others; Name=; Count=0}} _(showing 5 of 145 results)_ @@ -1039,37 +1124,23 @@ Get-PiHoleHistoryDatabase -PiHoleServer $PiHoleServer -Password $Password ``` -Timestamp : 9/27/2026 12:00:00 AM -Total : 1 -Cached : 1 -Blocked : 0 -Forwarded : 0 +Timestamp : 9/27/2026 1:00:00 PM +Total : 61 +Cached : 13 +Blocked : 24 +Forwarded : 24 -Timestamp : 9/27/2026 1:00:00 AM -Total : 1 -Cached : 1 -Blocked : 0 -Forwarded : 0 - -Timestamp : 9/27/2026 2:00:00 AM -Total : 1 -Cached : 1 -Blocked : 0 -Forwarded : 0 +Timestamp : 9/27/2026 1:10:00 PM +Total : 201 +Cached : 134 +Blocked : 58 +Forwarded : 9 -Timestamp : 9/27/2026 3:00:00 AM -Total : 1 -Cached : 1 +Timestamp : 9/27/2026 2:00:00 PM +Total : 3 +Cached : 3 Blocked : 0 Forwarded : 0 - -Timestamp : 9/27/2026 4:00:00 AM -Total : 1 -Cached : 1 -Blocked : 0 -Forwarded : 0 - -_(showing 5 of 8 results)_ ``` ### Get-PiHoleHistoryDatabaseClient @@ -1082,22 +1153,14 @@ Get-PiHoleHistoryDatabaseClient -PiHoleServer $PiHoleServer -Password $Password ``` -Timestamp : 9/27/2026 7:00:00 AM +Timestamp : 9/27/2026 2:00:00 PM Clients : @{ClientId=3; Count=1} -Timestamp : 9/27/2026 6:00:00 AM -Clients : @{ClientId=3; Count=1} +Timestamp : 9/27/2026 1:10:00 PM +Clients : @{ClientId=5; Count=1} -Timestamp : 9/27/2026 5:00:00 AM -Clients : @{ClientId=3; Count=1} - -Timestamp : 9/27/2026 4:00:00 AM -Clients : @{ClientId=3; Count=1} - -Timestamp : 9/27/2026 3:00:00 AM -Clients : @{ClientId=3; Count=1} - -_(showing 5 of 8 results)_ +Timestamp : 9/27/2026 1:00:00 PM +Clients : {@{ClientId=3; Count=1}, @{ClientId=2; Count=1}, @{ClientId=5; Count=1}} ``` ## Authentication @@ -1111,13 +1174,27 @@ Get-PiHoleCurrentAuthSession -PiHoleServer $PiHoleServer -Password $Password ``` Id : 0 +CurrentSession : False +Valid : True +TlsLogin : True +TlsMixed : False +LoginAt : 9/27/2026 1:51:01 PM +LastActive : 9/27/2026 2:03:07 PM +ValidUntil : 9/27/2026 2:33:07 PM +RemoteAddress : 192.168.1.162 +UserAgent : Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.0.0 Safari/537.36 Edg/153.0.0.0 +XForwardedFor : +App : False +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 2:15:19 PM +LastActive : 9/27/2026 2:15:24 PM +ValidUntil : 9/27/2026 2:45:24 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 : @@ -1130,13 +1207,13 @@ Cli : False _Deletes a session by its ID (as shown by Get-PiHoleCurrentAuthSession), not the caller's own session._ ```powershell -Remove-PiHoleAuthSession -PiHoleServer $PiHoleServer -Password $Password -Id 0 +Remove-PiHoleAuthSession -PiHoleServer $PiHoleServer -Password $Password -Id 1 ``` ``` -Id : 0 +Id : 1 Status : Removed ``` diff --git a/tests/Config/Add-PiHoleConfigArrayItem.Integration.Tests.ps1 b/tests/Config/Add-PiHoleConfigArrayItem.Integration.Tests.ps1 new file mode 100644 index 0000000..046d999 --- /dev/null +++ b/tests/Config/Add-PiHoleConfigArrayItem.Integration.Tests.ps1 @@ -0,0 +1,68 @@ +# 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: these tests add/remove a fake host entry using a TEST-NET-1 (RFC 5737) reserved IP that +# will never route anywhere real, rather than touching a genuine DNS record. They require the +# app password used here to have "app_sudo" enabled in Pi-hole (Settings > All Settings), same +# as the function itself - see its .DESCRIPTION. + +$script:ConfigAvailable = Test-Path (Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1') + +Describe 'Add-PiHoleConfigArrayItem (Integration)' -Tag 'Integration' { + BeforeAll { + Import-Module .\PiHoleShell\PiHoleShell.psm1 -Force + + $script:TestElement = 'dns/hosts' + $script:TestValue = '192.0.2.1 piholeshell-test-host.example.com' + + $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 host entry behind + Remove-PiHoleConfigArrayItem -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Element $script:TestElement -Value $script:TestValue -Restart $false -Confirm:$false -ErrorAction SilentlyContinue -WarningAction SilentlyContinue | Out-Null + } + } + + AfterAll { + if ($script:PiHoleServer) { + Remove-PiHoleConfigArrayItem -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Element $script:TestElement -Value $script:TestValue -Restart $false -Confirm:$false -ErrorAction SilentlyContinue | Out-Null + } + } + + It 'adds an item and returns a formatted object' -Skip:(-not $script:ConfigAvailable) { + $result = Add-PiHoleConfigArrayItem -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Element $script:TestElement -Value $script:TestValue -Restart $false + $result | Format-List | Out-String | Write-Host + + $result | Should -Not -BeNullOrEmpty + $result.Status | Should -Be 'Added' + + $hosts = (Get-PiHoleConfig -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl).Dns.Hosts + $hosts | Should -Contain $script:TestValue + + Remove-PiHoleConfigArrayItem -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Element $script:TestElement -Value $script:TestValue -Restart $false -Confirm:$false | Out-Null + } + + It 'errors when the item already exists' -Skip:(-not $script:ConfigAvailable) { + Add-PiHoleConfigArrayItem -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Element $script:TestElement -Value $script:TestValue -Restart $false | Out-Null + + $result = Add-PiHoleConfigArrayItem -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Element $script:TestElement -Value $script:TestValue -Restart $false -ErrorVariable errOut -ErrorAction SilentlyContinue + Write-Host "Error ($($errOut.Count) entries, showing last): [$($errOut[-1])]" + + $errOut | Should -Not -BeNullOrEmpty + + Remove-PiHoleConfigArrayItem -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Element $script:TestElement -Value $script:TestValue -Restart $false -Confirm:$false | Out-Null + } + + It 'errors when given a bad password' -Skip:(-not $script:ConfigAvailable) { + $result = Add-PiHoleConfigArrayItem -PiHoleServer $script:PiHoleServer -Password 'definitely-not-the-real-token' -IgnoreSsl $script:PiHoleIgnoreSsl -Element $script:TestElement -Value $script:TestValue -Restart $false -ErrorVariable errOut -ErrorAction SilentlyContinue + + $errOut | Should -Not -BeNullOrEmpty + } +} diff --git a/tests/Config/Get-PiHoleConfig.Integration.Tests.ps1 b/tests/Config/Get-PiHoleConfig.Integration.Tests.ps1 index 4461c04..8ef8b5d 100644 --- a/tests/Config/Get-PiHoleConfig.Integration.Tests.ps1 +++ b/tests/Config/Get-PiHoleConfig.Integration.Tests.ps1 @@ -28,6 +28,15 @@ Describe 'Get-PiHoleConfig (Integration)' -Tag 'Integration' { $result.Dhcp | Should -Not -BeNullOrEmpty } + It 'filters to just one element when -Element is specified' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleConfig -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Element 'dns/upstreams' + $result | Format-List | Out-String | Write-Host + + $result | Should -Not -BeNullOrEmpty + $result.Dns.Upstreams | Should -Not -BeNullOrEmpty + $result.Dhcp | Should -BeNullOrEmpty + } + It 'returns the raw API response when RawOutput is set' -Skip:(-not $script:ConfigAvailable) { $result = Get-PiHoleConfig -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -RawOutput $true $result | Format-List | Out-String | Write-Host diff --git a/tests/Config/Get-PiHoleConfigProperty.Integration.Tests.ps1 b/tests/Config/Get-PiHoleConfigProperty.Integration.Tests.ps1 new file mode 100644 index 0000000..2522f3b --- /dev/null +++ b/tests/Config/Get-PiHoleConfigProperty.Integration.Tests.ps1 @@ -0,0 +1,43 @@ +# 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-PiHoleConfigProperty (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 read-only properties as formatted objects' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleConfigProperty -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl + $result | Format-Table | Out-String | Write-Host + + # misc.readOnly is documented as always read-only, so it's a stable thing to assert on. + $result | Should -Not -BeNullOrEmpty + $result.Key | Should -Contain 'misc.readOnly' + } + + It 'returns the raw API response when RawOutput is set' -Skip:(-not $script:ConfigAvailable) { + $result = Get-PiHoleConfigProperty -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-PiHoleConfigProperty -PiHoleServer $script:PiHoleServer -Password 'definitely-not-the-real-token' -IgnoreSsl $script:PiHoleIgnoreSsl -ErrorVariable errOut -ErrorAction SilentlyContinue + + $errOut | Should -Not -BeNullOrEmpty + } +} diff --git a/tests/Config/Remove-PiHoleConfigArrayItem.Integration.Tests.ps1 b/tests/Config/Remove-PiHoleConfigArrayItem.Integration.Tests.ps1 new file mode 100644 index 0000000..5f9c24e --- /dev/null +++ b/tests/Config/Remove-PiHoleConfigArrayItem.Integration.Tests.ps1 @@ -0,0 +1,76 @@ +# 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: these tests add/remove a fake host entry using a TEST-NET-1 (RFC 5737) reserved IP that +# will never route anywhere real, rather than touching a genuine DNS record. They require the +# app password used here to have "app_sudo" enabled in Pi-hole (Settings > All Settings), same +# as the function itself - see its .DESCRIPTION. + +$script:ConfigAvailable = Test-Path (Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1') + +Describe 'Remove-PiHoleConfigArrayItem (Integration)' -Tag 'Integration' { + BeforeAll { + Import-Module .\PiHoleShell\PiHoleShell.psm1 -Force + + $script:TestElement = 'dns/hosts' + $script:TestValue = '192.0.2.1 piholeshell-test-host.example.com' + + $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 host entry behind + Remove-PiHoleConfigArrayItem -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Element $script:TestElement -Value $script:TestValue -Restart $false -Confirm:$false -ErrorAction SilentlyContinue -WarningAction SilentlyContinue | Out-Null + } + } + + AfterAll { + if ($script:PiHoleServer) { + Remove-PiHoleConfigArrayItem -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Element $script:TestElement -Value $script:TestValue -Restart $false -Confirm:$false -ErrorAction SilentlyContinue | Out-Null + } + } + + It 'removes an existing item and returns a formatted object' -Skip:(-not $script:ConfigAvailable) { + Add-PiHoleConfigArrayItem -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Element $script:TestElement -Value $script:TestValue -Restart $false | Out-Null + + $result = Remove-PiHoleConfigArrayItem -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Element $script:TestElement -Value $script:TestValue -Restart $false -Confirm:$false + $result | Format-List | Out-String | Write-Host + + $result | Should -Not -BeNullOrEmpty + $result.Status | Should -Be 'Removed' + + $hosts = (Get-PiHoleConfig -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl).Dns.Hosts + $hosts | Should -Not -Contain $script:TestValue + } + + It 'removes the item when RawOutput is set, even though the API returns no body' -Skip:(-not $script:ConfigAvailable) { + Add-PiHoleConfigArrayItem -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Element $script:TestElement -Value $script:TestValue -Restart $false | Out-Null + + # A successful delete is HTTP 204 No Content, so RawOutput is expected to be empty here - + # the item actually being gone afterward is the real signal of success. + Remove-PiHoleConfigArrayItem -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Element $script:TestElement -Value $script:TestValue -Restart $false -RawOutput $true -Confirm:$false + + $hosts = (Get-PiHoleConfig -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl).Dns.Hosts + $hosts | Should -Not -Contain $script:TestValue + } + + It 'errors when the item does not exist' -Skip:(-not $script:ConfigAvailable) { + $result = Remove-PiHoleConfigArrayItem -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Element $script:TestElement -Value $script:TestValue -Restart $false -Confirm:$false -ErrorVariable errOut -ErrorAction SilentlyContinue + + $errOut | Should -Not -BeNullOrEmpty + } + + It 'errors when given a bad password' -Skip:(-not $script:ConfigAvailable) { + Add-PiHoleConfigArrayItem -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Element $script:TestElement -Value $script:TestValue -Restart $false | Out-Null + + $result = Remove-PiHoleConfigArrayItem -PiHoleServer $script:PiHoleServer -Password 'definitely-not-the-real-token' -IgnoreSsl $script:PiHoleIgnoreSsl -Element $script:TestElement -Value $script:TestValue -Restart $false -Confirm:$false -ErrorVariable errOut -ErrorAction SilentlyContinue + + $errOut | Should -Not -BeNullOrEmpty + } +} diff --git a/tests/Config/Set-PiHoleConfig.Integration.Tests.ps1 b/tests/Config/Set-PiHoleConfig.Integration.Tests.ps1 new file mode 100644 index 0000000..2e14527 --- /dev/null +++ b/tests/Config/Set-PiHoleConfig.Integration.Tests.ps1 @@ -0,0 +1,69 @@ +# 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: these tests change and restore a genuinely harmless debug flag (debug.api) rather than +# anything that affects DNS resolution or other real behavior. They require the app password +# used here to have "app_sudo" enabled in Pi-hole (Settings > All Settings), same as the +# functions themselves - see their .DESCRIPTION. + +$script:ConfigAvailable = Test-Path (Join-Path (Split-Path $PSScriptRoot -Parent) 'IntegrationConfig.local.ps1') + +Describe 'Set-PiHoleConfig (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 'changes a setting and restores it, without restarting FTL' -Skip:(-not $script:ConfigAvailable) { + $original = (Get-PiHoleConfig -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl).Debug.Api + + try { + $result = Set-PiHoleConfig -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Settings @{ debug = @{ api = -not $original } } -Restart $false + $result | Format-List | Out-String | Write-Host + + $result | Should -Not -BeNullOrEmpty + $result.Debug.Api | Should -Be (-not $original) + } + finally { + Set-PiHoleConfig -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Settings @{ debug = @{ api = $original } } -Restart $false | Out-Null + } + } + + It 'returns the raw API response when RawOutput is set' -Skip:(-not $script:ConfigAvailable) { + $original = (Get-PiHoleConfig -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl).Debug.Api + + try { + $result = Set-PiHoleConfig -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Settings @{ debug = @{ api = -not $original } } -Restart $false -RawOutput $true + $result | Format-List | Out-String | Write-Host + + $result.config | Should -Not -BeNullOrEmpty + } + finally { + Set-PiHoleConfig -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Settings @{ debug = @{ api = $original } } -Restart $false | Out-Null + } + } + + It 'errors and gives a clear reason when the property can only be set in pihole.toml' -Skip:(-not $script:ConfigAvailable) { + $result = Set-PiHoleConfig -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Settings @{ misc = @{ readOnly = $true } } -Restart $false -ErrorVariable errOut -ErrorAction SilentlyContinue + Write-Host "Error ($($errOut.Count) entries, showing last): [$($errOut[-1])]" + + $errOut | Should -Not -BeNullOrEmpty + $errOut[-1] | Should -Match 'pihole.toml' + } + + It 'errors when given a bad password' -Skip:(-not $script:ConfigAvailable) { + $result = Set-PiHoleConfig -PiHoleServer $script:PiHoleServer -Password 'definitely-not-the-real-token' -IgnoreSsl $script:PiHoleIgnoreSsl -Settings @{ debug = @{ api = $true } } -Restart $false -ErrorVariable errOut -ErrorAction SilentlyContinue + + $errOut | Should -Not -BeNullOrEmpty + } +} diff --git a/tools/Update-ExampleOutput.ps1 b/tools/Update-ExampleOutput.ps1 index ad26ca4..21206c7 100644 --- a/tools/Update-ExampleOutput.ps1 +++ b/tools/Update-ExampleOutput.ps1 @@ -155,6 +155,48 @@ Add-Example -Category Config -FunctionName 'Get-PiHoleConfig' ` -Invocation 'Get-PiHoleConfig -PiHoleServer $PiHoleServer -Password $Password' ` -Result (Get-PiHoleConfig -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl) +Add-Example -Category Config -FunctionName 'Get-PiHoleConfigProperty' ` + -Invocation 'Get-PiHoleConfigProperty -PiHoleServer $PiHoleServer -Password $Password' ` + -Result (Get-PiHoleConfigProperty -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl) + +# Set-PiHoleConfig/Add-PiHoleConfigArrayItem/Remove-PiHoleConfigArrayItem all require the app +# password used here to have "app_sudo" enabled in Pi-hole (Settings > All Settings) - Pi-hole +# blocks config changes from app passwords by default. Skip these three with a placeholder if +# that isn't the case, rather than failing the whole run. +$appSudoEnabled = (Get-PiHoleConfig -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl).Webserver.Api.AppSudo +if ($appSudoEnabled) { + $originalDebugApi = (Get-PiHoleConfig -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl).Debug.Api + $toggledDebugApi = -not $originalDebugApi + + Add-Example -Category Config -FunctionName 'Set-PiHoleConfig' ` + -Invocation "Set-PiHoleConfig -PiHoleServer `$PiHoleServer -Password `$Password -Settings @{ debug = @{ api = `$$($toggledDebugApi.ToString().ToLower()) } }" ` + -Result (Set-PiHoleConfig -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl -Settings @{ debug = @{ api = $toggledDebugApi } } -Restart $false) + Set-PiHoleConfig -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl -Settings @{ debug = @{ api = $originalDebugApi } } -Restart $false | Out-Null + + $docsHostEntry = '192.0.2.1 piholeshell-docs-example.com' + Invoke-Quietly { Remove-PiHoleConfigArrayItem -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl -Element 'dns/hosts' -Value $docsHostEntry -Restart $false -Confirm:$false } + + Add-Example -Category Config -FunctionName 'Add-PiHoleConfigArrayItem' ` + -Invocation "Add-PiHoleConfigArrayItem -PiHoleServer `$PiHoleServer -Password `$Password -Element `"dns/hosts`" -Value `"$docsHostEntry`"" ` + -Result (Add-PiHoleConfigArrayItem -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl -Element 'dns/hosts' -Value $docsHostEntry -Restart $false) + + Add-Example -Category Config -FunctionName 'Remove-PiHoleConfigArrayItem' ` + -Invocation "Remove-PiHoleConfigArrayItem -PiHoleServer `$PiHoleServer -Password `$Password -Element `"dns/hosts`" -Value `"$docsHostEntry`"" ` + -Result (Remove-PiHoleConfigArrayItem -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl -Element 'dns/hosts' -Value $docsHostEntry -Restart $false -Confirm:$false) +} +else { + $appSudoNote = 'Requires "app_sudo" enabled for this app password (Settings > All Settings in your Pi-hole admin UI) - not captured this run since it is disabled here.' + Add-Example -Category Config -FunctionName 'Set-PiHoleConfig' ` + -Invocation 'Set-PiHoleConfig -PiHoleServer $PiHoleServer -Password $Password -Settings @{ debug = @{ api = $true } }' ` + -Note $appSudoNote -Skipped + Add-Example -Category Config -FunctionName 'Add-PiHoleConfigArrayItem' ` + -Invocation 'Add-PiHoleConfigArrayItem -PiHoleServer $PiHoleServer -Password $Password -Element "dns/hosts" -Value "192.0.2.1 example.com"' ` + -Note $appSudoNote -Skipped + Add-Example -Category Config -FunctionName 'Remove-PiHoleConfigArrayItem' ` + -Invocation 'Remove-PiHoleConfigArrayItem -PiHoleServer $PiHoleServer -Password $Password -Element "dns/hosts" -Value "192.0.2.1 example.com"' ` + -Note $appSudoNote -Skipped +} + Add-Example -Category Config -FunctionName 'Get-PiHolePadd' ` -Invocation 'Get-PiHolePadd -PiHoleServer $PiHoleServer -Password $Password' ` -Result (Get-PiHolePadd -PiHoleServer $PiHoleServer -Password $PiHoleToken -IgnoreSsl $PiHoleIgnoreSsl) From 7caed4a25afa31e404579298b958d5dde767745e Mon Sep 17 00:00:00 2001 From: Mike Madeja Date: Mon, 28 Sep 2026 15:17:18 -0500 Subject: [PATCH 2/2] docs: regenerate README and EXAMPLES after merging develop Resolves the README.md/docs/EXAMPLES.md conflicts from merging develop (which now includes PR #122's Client Management and Network Information) by regenerating both from the merged source instead of hand-resolving generated-file diffs. Co-Authored-By: Claude Sonnet 5 --- README.md | 15 ++ docs/EXAMPLES.md | 523 +++++++++++++++++++++++++++++++++-------------- 2 files changed, 388 insertions(+), 150 deletions(-) diff --git a/README.md b/README.md index ad4c0a5..798ebf2 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 | @@ -170,10 +180,15 @@ 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-PiHoleConfigArrayItem` | Delete config array item | | `Remove-PiHoleInfoMessage` | Delete a Pi-hole diagnosis message | +| `Remove-PiHoleNetworkDevice` | Delete a device from the network table | | `Set-PiHoleConfig` | Change configuration of your Pi-hole | ### Authentication diff --git a/docs/EXAMPLES.md b/docs/EXAMPLES.md index 9f97dcf..7bb7f63 100644 --- a/docs/EXAMPLES.md +++ b/docs/EXAMPLES.md @@ -100,8 +100,8 @@ Name : PiHoleShellDocsExampleGroup Comment : Example group Enabled : True Id : 17 -DateAdded : 9/27/2026 2:18:02 PM -DateModified : 9/27/2026 2:18:02 PM +DateAdded : 9/28/2026 3:05:48 PM +DateModified : 9/28/2026 3:05:48 PM ``` ### Get-PiHoleGroup @@ -137,8 +137,8 @@ Name : PiHoleShellDocsExampleGroup Comment : Example group Enabled : True Id : 17 -DateAdded : 9/27/2026 2:18:02 PM -DateModified : 9/27/2026 2:18:02 PM +DateAdded : 9/28/2026 3:05:48 PM +DateModified : 9/28/2026 3:05:48 PM ``` ### Update-PiHoleGroup @@ -153,8 +153,8 @@ Name : PiHoleShellDocsExampleGroup Comment : Example group Enabled : False Id : 17 -DateAdded : 9/27/2026 2:18:02 PM -DateModified : 9/27/2026 2:18:21 PM +DateAdded : 9/28/2026 3:05:48 PM +DateModified : 9/28/2026 3:06:06 PM ``` ### Remove-PiHoleGroup @@ -184,8 +184,8 @@ Comment : Example list Groups : {Default} Enabled : True Id : 79 -DateAdded : 9/27/2026 2:18:57 PM -DateModified : 9/27/2026 2:18:57 PM +DateAdded : 9/28/2026 3:06:43 PM +DateModified : 9/28/2026 3:06:43 PM Type : Block DateUpdated : Number : 0 @@ -288,8 +288,8 @@ Comment : Example list Groups : {Default} Enabled : False Id : 79 -DateAdded : 9/27/2026 2:18:57 PM -DateModified : 9/27/2026 2:19:31 PM +DateAdded : 9/28/2026 3:06:43 PM +DateModified : 9/28/2026 3:07:17 PM Type : Block DateUpdated : Number : 0 @@ -415,8 +415,8 @@ Comment : Example domain Groups : {Default} Enabled : True Id : 16 -DateAdded : 9/27/2026 2:20:09 PM -DateModified : 9/27/2026 2:20:09 PM +DateAdded : 9/28/2026 3:07:54 PM +DateModified : 9/28/2026 3:07:54 PM ``` ### Get-PiHoleDomain @@ -435,8 +435,8 @@ Comment : Example domain Groups : {Default} Enabled : True Id : 16 -DateAdded : 9/27/2026 2:20:09 PM -DateModified : 9/27/2026 2:20:09 PM +DateAdded : 9/28/2026 3:07:54 PM +DateModified : 9/28/2026 3:07:54 PM ``` ### Update-PiHoleDomain @@ -455,8 +455,8 @@ Comment : Example domain Groups : {Default} Enabled : False Id : 16 -DateAdded : 9/27/2026 2:20:09 PM -DateModified : 9/27/2026 2:20:42 PM +DateAdded : 9/28/2026 3:07:54 PM +DateModified : 9/28/2026 3:08:28 PM ``` ### Remove-PiHoleDomain @@ -473,6 +473,92 @@ 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 : 17 +DateAdded : 9/28/2026 3:08:58 PM +DateModified : 9/28/2026 3:08:58 PM +``` + +### Get-PiHoleClient + +```powershell +Get-PiHoleClient -PiHoleServer $PiHoleServer -Password $Password +``` + +``` + +Client : 192.168.99.99 +Name : +Comment : Example client +Groups : {Default} +Id : 17 +DateAdded : 9/28/2026 3:08:58 PM +DateModified : 9/28/2026 3:08:58 PM +``` + +### Get-PiHoleClientSuggestion + +```powershell +Get-PiHoleClientSuggestion -PiHoleServer $PiHoleServer -Password $Password +``` + +``` + +HwAddr : ip-127.0.0.1 +MacVendor : +LastQuery : 9/28/2026 3:00:00 PM +Addresses : 127.0.0.1 +Names : localhost + +HwAddr : 74:56:3c:bb:f4:0b +MacVendor : Giga-Byte Technology Co.,Ltd. +LastQuery : 9/27/2026 5:34:26 PM +Addresses : 192.168.1.162 +Names : +``` + +### 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 : 17 +DateAdded : 9/28/2026 3:08:58 PM +DateModified : 9/28/2026 3:09:37 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 : 265 -Blocked : 82 -PercentBlocked : 30.943395614624 -UniqueDomains : 21 -Forwarded : 33 -Cached : 150 +Total : 299 +Blocked : 103 +PercentBlocked : 34.4481620788574 +UniqueDomains : 22 +Forwarded : 39 +Cached : 157 Frequency : 0 -Types : @{A=129; AAAA=130; ANY=0; SRV=0; SOA=0; PTR=6; TXT=0; NAPTR=0; MX=0; DS=0; RRSIG=0; DNSKEY=0; NS=0; SVCB=0; HTTPS=0; OTHER=0} -Status : @{Unknown=0; Gravity=82; Forwarded=33; Cache=123; 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=27; ExternalBlockedEde15=0} -Replies : @{Unknown=8; Nodata=20; Nxdomain=2; Cname=0; Ip=231; Domain=4; Rrname=0; ServFail=0; Refused=0; Notimp=0; Other=0; Dnssec=0; None=0; Blob=0} +Types : @{A=134; AAAA=133; ANY=0; SRV=0; SOA=0; PTR=32; TXT=0; NAPTR=0; MX=0; DS=0; RRSIG=0; DNSKEY=0; NS=0; SVCB=0; HTTPS=0; OTHER=0} +Status : @{Unknown=0; Gravity=103; Forwarded=39; Cache=127; 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=30; ExternalBlockedEde15=0} +Replies : @{Unknown=13; Nodata=24; Nxdomain=3; Cname=0; Ip=230; Domain=29; Rrname=0; ServFail=0; Refused=0; Notimp=0; Other=0; Dnssec=0; None=0; Blob=0} Clients : @{Active=2; Total=2} -Gravity : @{DomainsBeingBlocked=496171; LastUpdate=1790530224} +Gravity : @{DomainsBeingBlocked=496171; LastUpdate=1790545835} ``` ### Get-PiHoleStatsRecentBlocked @@ -505,7 +591,7 @@ Get-PiHoleStatsRecentBlocked -PiHoleServer $PiHoleServer -Password $Password ``` -Blocked : adservice.google.com +Blocked : googlesyndication.com ``` ### Get-PiHoleStatsQueryType @@ -517,10 +603,10 @@ Get-PiHoleStatsQueryType -PiHoleServer $PiHoleServer -Password $Password ``` Type : A -Count : 129 +Count : 134 Type : AAAA -Count : 130 +Count : 133 Type : ANY Count : 0 @@ -542,21 +628,21 @@ Get-PiHoleStatsTopDomain -PiHoleServer $PiHoleServer -Password $Password ``` -Domain : python.org -Count : 22 +Domain : bbc.com +Count : 23 -Domain : microsoft.com -Count : 20 +Domain : 1.0.0.127.in-addr.arpa +Count : 23 Domain : nytimes.com -Count : 18 - -Domain : example.com -Count : 18 +Count : 20 -Domain : wikipedia.org +Domain : cloudflare.com Count : 16 +Domain : github.com +Count : 15 + _(showing 5 of 10 results)_ ``` @@ -570,11 +656,11 @@ Get-PiHoleStatsTopClient -PiHoleServer $PiHoleServer -Password $Password IP : 192.168.1.162 Name : -Count : 259 +Count : 267 IP : 127.0.0.1 Name : localhost -Count : 6 +Count : 32 ``` ### Get-PiHoleStatsUpstream @@ -585,9 +671,9 @@ Get-PiHoleStatsUpstream -PiHoleServer $PiHoleServer -Password $Password ``` -TotalQueries : 265 -ForwardedQueries : 33 -Upstreams : {@{Ip=blocklist; Name=blocklist; Port=-1; Count=82; ResponseTime=0; Variance=0}, @{Ip=cache; Name=cache; Port=-1; Count=150; ResponseTime=0; Variance=0}, @{Ip=8.8.8.8; Name=dns.google; Port=53; Count=33; ResponseTime=0.0300731555275295; Variance=0.0024513289105294}} +TotalQueries : 299 +ForwardedQueries : 39 +Upstreams : {@{Ip=blocklist; Name=blocklist; Port=-1; Count=103; ResponseTime=0; Variance=0}, @{Ip=cache; Name=cache; Port=-1; Count=157; ResponseTime=0; Variance=0}, @{Ip=8.8.8.8; Name=dns.google; Port=53; Count=21; ResponseTime=0.0285110175609589; Variance=0.00743893922252483}, @{Ip=8.8.4.4; Name=dns.google; Port=53; Count=18; ResponseTime=0.0329826772212982; Variance=0.00822325505214882}} ``` ### Get-PiHoleStatsQuerySuggestions @@ -598,10 +684,10 @@ Get-PiHoleStatsQuerySuggestions -PiHoleServer $PiHoleServer -Password $Password ``` -Domain : {python.org, microsoft.com, nytimes.com, example.com…} +Domain : {bbc.com, 1.0.0.127.in-addr.arpa, nytimes.com, cloudflare.com…} ClientIp : {192.168.1.162, 127.0.0.1} ClientName : {localhost} -Upstream : {blocklist, cache, 8.8.8.8#53 (dns.google), permitted} +Upstream : {blocklist, cache, 8.8.8.8#53 (dns.google), 8.8.4.4#53 (dns.google)…} Type : {A, AAAA, ANY, SRV…} Status : {UNKNOWN, GRAVITY, FORWARDED, CACHE…} Reply : {UNKNOWN, NODATA, NXDOMAIN, CNAME…} @@ -618,10 +704,10 @@ Get-PiHoleStatsDatabaseSummary -PiHoleServer $PiHoleServer -Password $Password ``` -SumQueries : 265 -SumBlocked : 82 -PercentBlocked : 30.943395614624 -TotalClients : 3 +SumQueries : 8 +SumBlocked : 0 +PercentBlocked : 0 +TotalClients : 1 ``` ### Get-PiHoleStatsDatabaseQueryType @@ -635,7 +721,7 @@ Get-PiHoleStatsDatabaseQueryType -PiHoleServer $PiHoleServer -Password $Password ``` Type : A -Count : 130 +Count : 0 Type : AAAA Count : 0 @@ -647,7 +733,7 @@ Type : SRV Count : 0 Type : SOA -Count : 6 +Count : 8 _(showing 5 of 16 results)_ ``` @@ -662,22 +748,8 @@ Get-PiHoleStatsDatabaseTopDomain -PiHoleServer $PiHoleServer -Password $Password ``` -Domain : python.org -Count : 22 - -Domain : microsoft.com -Count : 20 - -Domain : nytimes.com -Count : 18 - -Domain : example.com -Count : 18 - -Domain : wikipedia.org -Count : 16 - -_(showing 5 of 10 results)_ +Domain : 1.0.0.127.in-addr.arpa +Count : 8 ``` ### Get-PiHoleStatsDatabaseTopClient @@ -690,17 +762,9 @@ Get-PiHoleStatsDatabaseTopClient -PiHoleServer $PiHoleServer -Password $Password ``` -IP : 192.168.1.162 -Name : -Count : 177 - IP : 127.0.0.1 Name : localhost -Count : 4 - -IP : 127.0.0.1 -Name : -Count : 2 +Count : 8 ``` ### Get-PiHoleStatsDatabaseUpstream @@ -713,9 +777,9 @@ Get-PiHoleStatsDatabaseUpstream -PiHoleServer $PiHoleServer -Password $Password ``` -TotalQueries : 265 -ForwardedQueries : 33 -Upstreams : {@{Ip=1; Name=; Port=4294967295; Count=1; ResponseTime=0; Variance=0}, @{Ip=2; Name=; Port=4294967295; Count=32; ResponseTime=0; Variance=0}, @{Ip=cache; Name=cache; Port=-1; Count=123; ResponseTime=0; Variance=0}, @{Ip=blocklist; Name=blocklist; Port=-1; Count=109; ResponseTime=0; Variance=0}} +TotalQueries : 8 +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}} ``` ## Configuration & Diagnostics @@ -809,20 +873,20 @@ CpuPercent : 0 MemoryPercent : 0 ActiveClients : 2 Blocking : enabled -Cache : @{Size=10000; Inserted=127; Evicted=0} +Cache : @{Size=10000; Inserted=131; Evicted=0} Config : @{DhcpActive=False; DhcpStart=; DhcpEnd=; DhcpIpv6=False; DnsDnssec=False; DnsDomain=lan; DnsNumUpstreams=2; DnsPort=53; DnsrevServerAactive=False; PrivacyLevel=0} GravitySize : 496171 HostModel : Raspberry Pi Zero W Rev 1.1 IFace : @{v4=; v6=} NodeName : dns3.localdomain Pid : 389 -Queries : @{Total=265; Blocked=82; PercentBlocked=30.943395614624; QueryFrequency=0} -RecentBlocked : adservice.google.com -Sensors : @{CpuTemp=37.932; HotLimit=60; Unit=C} -System : @{Uptime=653422; Memory=; Procs=80; Cpu=; Ftl=} -TopBlocked : analytics.google.com +Queries : @{Total=299; Blocked=103; PercentBlocked=34.4481620788574; QueryFrequency=0} +RecentBlocked : googlesyndication.com +Sensors : @{CpuTemp=39.008; HotLimit=60; Unit=C} +System : @{Uptime=742734; Memory=; Procs=72; Cpu=; Ftl=} +TopBlocked : googleadservices.com TopClient : 192.168.1.162 -TopDomain : python.org +TopDomain : bbc.com Version : @{Core=; Web=; Ftl=; Docker=} ``` @@ -859,11 +923,11 @@ Get-PiHoleInfoSystem -PiHoleServer $PiHoleServer -Password $Password ``` -Uptime : 653433 +Uptime : 742744 Memory : @{Ram=; Swap=} -Procs : 81 -Cpu : @{NumProcessors=1; PercentCpu=96.1999969482422; Load=} -Ftl : @{PercentMemory=3.29122042655945; PercentCpu=95.3000030517578} +Procs : 72 +Cpu : @{NumProcessors=1; PercentCpu=96.9000015258789; Load=} +Ftl : @{PercentMemory=3.11284995079041; PercentCpu=96.3000030517578} ``` ### Get-PiHoleInfoFtl @@ -879,11 +943,11 @@ PrivacyLevel : 0 QueryFrequency : 0 Clients : @{Total=2; Active=2} Pid : 389 -Uptime : 6534699.497781 -PercentMemory : 3.29122042655945 -PercentCpu : 96.1999969482422 +Uptime : 80190954.666427 +PercentMemory : 3.11284995079041 +PercentCpu : 96.3000030517578 AllowDestructive : True -Dnsmasq : @{DnsCacheInserted=127; DnsCacheLiveFreed=0; DnsQueriesForwarded=60; DnsAuthAnswered=0; DnsLocalAnswered=205; DnsStaleAnswered=27; 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=131; DnsCacheLiveFreed=0; DnsQueriesForwarded=69; DnsAuthAnswered=0; DnsLocalAnswered=230; DnsStaleAnswered=30; 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 @@ -895,7 +959,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 : 37.932 +CpuTemp : 40.084 HotLimit : 60 Unit : C ``` @@ -912,12 +976,12 @@ Size : 13983744 Type : Regular file Mode : rw-r----- AccessTime : 6/25/2025 12:32:44 AM -ModifiedTime : 9/27/2026 12:28:00 PM -ChangeTime : 9/27/2026 12:28:00 PM +ModifiedTime : 9/28/2026 2:23:00 PM +ChangeTime : 9/28/2026 2:23:00 PM Owner : @{User=; Group=} -Queries : 265 -EarliestTimestamp : 9/26/2026 2:10:00 PM -QueriesDisk : 265 +Queries : 299 +EarliestTimestamp : 9/27/2026 3:00:00 PM +QueriesDisk : 299 EarliestTimestampDisk : SqliteVersion : 3.53.1 ``` @@ -987,7 +1051,7 @@ Get-PiHoleInfoMessage -PiHoleServer $PiHoleServer -Password $Password ``` Id : 1 -Timestamp : 9/27/2026 1:13:02 PM +Timestamp : 9/27/2026 5:33:13 PM Type : LOAD Plain : Long-term load (15min avg) larger than number of processors: 1.1 > 1 Html : Long-term load (15min avg) larger than number of processors: 1.1 > 1
This may slow down DNS resolution and can cause bottlenecks. @@ -1025,8 +1089,8 @@ Get-PiHoleLogWebserver -PiHoleServer $PiHoleServer -Password $Password ``` -Log : {@{Timestamp=9/27/2026 12:28:02 PM; Message=Initializing HTTP server on ports "8089,8489s"; Priority=}, @{Timestamp=9/27/2026 1:54:43 PM; Message=ACCESS: 192.168.1.162 - - [27/Sep/2026:18:54:42 +0000] "PATCH /api/config?restart=false HTTP/1.1" 200 4590 - Mozilla/5.0 (Windows NT 10.0; Microsoft Windows 10.0.26200; en-US) PowerShell/7.6.6; Priority=}, @{Timestamp=9/27/2026 1:54:44 PM; Message=ACCESS: 192.168.1.162 - - [27/Sep/2026:18:54:43 +0000] "DELETE /api/auth HTTP/1.1" 204 558 - Mozilla/5.0 (Windows NT 10.0; Microsoft Windows 10.0.26200; en-US) PowerShell/7.6.6; Priority=}, @{Timestamp=9/27/2026 1:54:48 PM; Message=ACCESS: 192.168.1.162 - - [27/Sep/2026:18:54:47 +0000] "GET /api/dns/blocking HTTP/1.1" 200 692 https://dns3.localdomain:8489/admin/settings/all Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.0.0 Safari/537.36...; Priority=}…} -NextID : 14 +Log : {@{Timestamp=9/27/2026 4:48:13 PM; Message=Initializing HTTP server on ports "8089,8489s"; Priority=}, @{Timestamp=9/28/2026 3:04:02 PM; Message=ACCESS: 192.168.1.162 - - [28/Sep/2026:20:04:01 +0000] "PATCH /api/config?restart=false HTTP/1.1" 200 4590 - Mozilla/5.0 (Windows NT 10.0; Microsoft Windows 10.0.26200; en-US) PowerShell/7.6.6; Priority=}, @{Timestamp=9/28/2026 3:04:03 PM; Message=ACCESS: 192.168.1.162 - - [28/Sep/2026:20:04:02 +0000] "DELETE /api/auth HTTP/1.1" 204 558 - Mozilla/5.0 (Windows NT 10.0; Microsoft Windows 10.0.26200; en-US) PowerShell/7.6.6; Priority=}, @{Timestamp=9/28/2026 3:04:07 PM; Message=ACCESS: 192.168.1.162 - - [28/Sep/2026:20:04:02 +0000] "POST /api/auth HTTP/1.1" 200 808 - Mozilla/5.0 (Windows NT 10.0; Microsoft Windows 10.0.26200; en-US) PowerShell/7.6.6; Priority=}} +NextID : 4 Pid : 389 File : /var/log/pihole/webserver.log ``` @@ -1055,31 +1119,31 @@ Get-PiHoleHistory -PiHoleServer $PiHoleServer -Password $Password ``` -Timestamp : 9/26/2026 2:25:00 PM +Timestamp : 9/27/2026 3:15:00 PM Total : 0 Cached : 0 Blocked : 0 Forwarded : 0 -Timestamp : 9/26/2026 2:35:00 PM +Timestamp : 9/27/2026 3:25:00 PM Total : 0 Cached : 0 Blocked : 0 Forwarded : 0 -Timestamp : 9/26/2026 2:45:00 PM +Timestamp : 9/27/2026 3:35:00 PM Total : 0 Cached : 0 Blocked : 0 Forwarded : 0 -Timestamp : 9/26/2026 2:55:00 PM +Timestamp : 9/27/2026 3:45:00 PM Total : 0 Cached : 0 Blocked : 0 Forwarded : 0 -Timestamp : 9/26/2026 3:05:00 PM +Timestamp : 9/27/2026 3:55:00 PM Total : 0 Cached : 0 Blocked : 0 @@ -1096,19 +1160,19 @@ Get-PiHoleHistoryClient -PiHoleServer $PiHoleServer -Password $Password ``` -Timestamp : 9/26/2026 2:25:00 PM +Timestamp : 9/27/2026 3:15:00 PM Clients : {@{IP=192.168.1.162; Name=; Count=0}, @{IP=others; Name=; Count=0}} -Timestamp : 9/26/2026 2:35:00 PM +Timestamp : 9/27/2026 3:25:00 PM Clients : {@{IP=192.168.1.162; Name=; Count=0}, @{IP=others; Name=; Count=0}} -Timestamp : 9/26/2026 2:45:00 PM +Timestamp : 9/27/2026 3:35:00 PM Clients : {@{IP=192.168.1.162; Name=; Count=0}, @{IP=others; Name=; Count=0}} -Timestamp : 9/26/2026 2:55:00 PM +Timestamp : 9/27/2026 3:45:00 PM Clients : {@{IP=192.168.1.162; Name=; Count=0}, @{IP=others; Name=; Count=0}} -Timestamp : 9/26/2026 3:05:00 PM +Timestamp : 9/27/2026 3:55:00 PM Clients : {@{IP=192.168.1.162; Name=; Count=0}, @{IP=others; Name=; Count=0}} _(showing 5 of 145 results)_ @@ -1124,23 +1188,37 @@ Get-PiHoleHistoryDatabase -PiHoleServer $PiHoleServer -Password $Password ``` -Timestamp : 9/27/2026 1:00:00 PM -Total : 61 -Cached : 13 -Blocked : 24 -Forwarded : 24 +Timestamp : 9/28/2026 8:00:00 AM +Total : 1 +Cached : 1 +Blocked : 0 +Forwarded : 0 -Timestamp : 9/27/2026 1:10:00 PM -Total : 201 -Cached : 134 -Blocked : 58 -Forwarded : 9 +Timestamp : 9/28/2026 9:00:00 AM +Total : 1 +Cached : 1 +Blocked : 0 +Forwarded : 0 + +Timestamp : 9/28/2026 10:00:00 AM +Total : 1 +Cached : 1 +Blocked : 0 +Forwarded : 0 -Timestamp : 9/27/2026 2:00:00 PM -Total : 3 -Cached : 3 +Timestamp : 9/28/2026 11:00:00 AM +Total : 1 +Cached : 1 Blocked : 0 Forwarded : 0 + +Timestamp : 9/28/2026 12:00:00 PM +Total : 1 +Cached : 1 +Blocked : 0 +Forwarded : 0 + +_(showing 5 of 8 results)_ ``` ### Get-PiHoleHistoryDatabaseClient @@ -1153,14 +1231,173 @@ Get-PiHoleHistoryDatabaseClient -PiHoleServer $PiHoleServer -Password $Password ``` -Timestamp : 9/27/2026 2:00:00 PM +Timestamp : 9/28/2026 3:00:00 PM +Clients : @{ClientId=3; Count=1} + +Timestamp : 9/28/2026 2:00:00 PM +Clients : @{ClientId=3; Count=1} + +Timestamp : 9/28/2026 1:00:00 PM Clients : @{ClientId=3; Count=1} -Timestamp : 9/27/2026 1:10:00 PM -Clients : @{ClientId=5; Count=1} +Timestamp : 9/28/2026 12:00:00 PM +Clients : @{ClientId=3; Count=1} + +Timestamp : 9/28/2026 11:00:00 AM +Clients : @{ClientId=3; Count=1} + +_(showing 5 of 8 results)_ +``` + +### Get-PiHoleNetworkGateway + +```powershell +Get-PiHoleNetworkGateway -PiHoleServer $PiHoleServer -Password $Password +``` + +``` + +Family : inet +Interface : wlan0 +Address : 192.168.1.1 +Local : 192.168.1.248 +``` + +### Get-PiHoleNetworkRoute + +```powershell +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 1:00:00 PM -Clients : {@{ClientId=3; Count=1}, @{ClientId=2; Count=1}, @{ClientId=5; 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=1789883140.02; Tstamp=1789883140.02}, @{Family=inet6; Scope=host; Flags=permanent; Prefixlen=128; Address=::1; AddressType=loopback; Prefered=4294967295; Valid=4294967295; Cstamp=1789883140.02; Tstamp=1789883140.02}} + +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=65600; Valid=65600; Cstamp=1789883160.32; Tstamp=1790175981.82}, @{Family=inet6; Scope=link; Flags=permanent; Prefixlen=64; Address=fe80::ba27:ebff:fe11:a4d5; AddressType=link-local (LL); Prefered=4294967295; Valid=4294967295; Cstamp=1789883159.81; Tstamp=1789883159.81}} +``` + +### Get-PiHoleNetworkDevice + +_Shows up to 10 devices by default; pass -MaxDevices for more._ + +```powershell +Get-PiHoleNetworkDevice -PiHoleServer $PiHoleServer -Password $Password +``` + +``` + +Id : 2 +HwAddr : ip-127.0.0.1 +Interface : lo +FirstSeen : 9/27/2026 5:20:13 PM +LastQuery : 9/28/2026 3:00:00 PM +NumQueries : 32 +MacVendor : +Ips : @{Ip=127.0.0.1; Name=localhost; LastSeen=9/28/2026 3:11:00 PM; NameUpdated=9/28/2026 3:11:00 PM} + +Id : 1 +HwAddr : 74:56:3c:bb:f4:0b +Interface : wlan0 +FirstSeen : 9/27/2026 5:20:13 PM +LastQuery : 9/27/2026 5:34:26 PM +NumQueries : 267 +MacVendor : Giga-Byte Technology Co.,Ltd. +Ips : @{Ip=192.168.1.162; Name=; LastSeen=9/28/2026 3:11:00 PM; NameUpdated=12/31/1969 6:00:00 PM} +``` + +### 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 @@ -1174,27 +1411,13 @@ Get-PiHoleCurrentAuthSession -PiHoleServer $PiHoleServer -Password $Password ``` Id : 0 -CurrentSession : False -Valid : True -TlsLogin : True -TlsMixed : False -LoginAt : 9/27/2026 1:51:01 PM -LastActive : 9/27/2026 2:03:07 PM -ValidUntil : 9/27/2026 2:33:07 PM -RemoteAddress : 192.168.1.162 -UserAgent : Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.0.0 Safari/537.36 Edg/153.0.0.0 -XForwardedFor : -App : False -Cli : False - -Id : 1 CurrentSession : True Valid : True TlsLogin : True TlsMixed : False -LoginAt : 9/27/2026 2:15:19 PM -LastActive : 9/27/2026 2:15:24 PM -ValidUntil : 9/27/2026 2:45:24 PM +LoginAt : 9/28/2026 3:03:07 PM +LastActive : 9/28/2026 3:03:11 PM +ValidUntil : 9/28/2026 3:33:11 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 : @@ -1207,13 +1430,13 @@ Cli : False _Deletes a session by its ID (as shown by Get-PiHoleCurrentAuthSession), not the caller's own session._ ```powershell -Remove-PiHoleAuthSession -PiHoleServer $PiHoleServer -Password $Password -Id 1 +Remove-PiHoleAuthSession -PiHoleServer $PiHoleServer -Password $Password -Id 0 ``` ``` -Id : 1 +Id : 0 Status : Removed ```