[pihole] New binding PiHole (#16627)

* Init Pi-hole binding

Signed-off-by: Martin Grześlowski <martin.grzeslowski@gmail.com>
This commit is contained in:
Martin 2024-07-09 16:07:47 +02:00 committed by GitHub
parent 7755681749
commit 4f322cc663
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 1506 additions and 0 deletions

View File

@ -282,6 +282,7 @@
/bundles/org.openhab.binding.pegelonline/ @weymann
/bundles/org.openhab.binding.pentair/ @jsjames
/bundles/org.openhab.binding.phc/ @gnlpfjh
/bundles/org.openhab.binding.pihole/ @magx2
/bundles/org.openhab.binding.pilight/ @stefanroellin @niklasdoerfler
/bundles/org.openhab.binding.pioneeravr/ @Stratehm
/bundles/org.openhab.binding.pixometer/ @Confectrician

View File

@ -1401,6 +1401,11 @@
<artifactId>org.openhab.binding.phc</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.pihole</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.pilight</artifactId>

View File

@ -0,0 +1,13 @@
This content is produced and maintained by the openHAB project.
* Project home: https://www.openhab.org
== Declared Project Licenses
This program and the accompanying materials are made available under the terms
of the Eclipse Public License 2.0 which is available at
https://www.eclipse.org/legal/epl-2.0/.
== Source Code
https://github.com/openhab/openhab-addons

View File

@ -0,0 +1,165 @@
# Pi-hole Binding
The Pi-hole Binding is a bridge between openHAB and Pi-hole, enabling users to integrate Pi-hole statistics and controls into their home automation setup. Pi-hole is a DNS-based ad blocker that can run on a variety of platforms, including Raspberry Pi.
Pi-hole is a powerful network-level advertisement and internet tracker blocking application.
By intercepting DNS requests, it can prevent unwanted content from being displayed on devices connected to your network.
The Pi-hole Binding allows you to monitor Pi-hole statistics and control its functionality directly from your openHAB setup.
### Features
- Real-time Statistics: Monitor key metrics such as the number of domains being blocked, DNS queries made today, ads blocked today, and more.
- Control: Enable or disable Pi-hole's blocking functionality, configure blocking options, and adjust privacy settings directly from openHAB.
- Integration: Seamlessly integrate Pi-hole data and controls with other openHAB items and rules to create advanced automation scenarios.
## Supported Things
- `server`: Pi-hole server
## Thing Configuration
### `server` Thing Configuration
| Name | Type | Description | Default | Required | Advanced |
|-----------------|---------|-------------------------------------------------------------------------------------------|---------|----------|----------|
| hostname | text | Hostname or IP address of the device | N/A | yes | no |
| token | text | Token to access the device. To generate token go to `settings` > `API` > `Show API token` | N/A | yes | no |
| refreshInterval | integer | Interval the device is polled in sec. | 600 | no | yes |
## Channels
| Channel | Type | Read/Write | Description |
|-------------------------|--------|------------|------------------------------------------------------------|
| domains-being-blocked | Number | RO | The total number of domains currently being blocked. |
| dns-queries-today | Number | RO | The count of DNS queries made today. |
| ads-blocked-today | Number | RO | The number of ads blocked today. |
| ads-percentage-today | Number | RO | The percentage of ads blocked today. |
| unique-domains | Number | RO | The count of unique domains queried. |
| queries-forwarded | Number | RO | The number of queries forwarded to an external DNS server. |
| queries-cached | Number | RO | The number of queries served from the cache. |
| clients-ever-seen | Number | RO | The total number of unique clients ever seen. |
| unique-clients | Number | RO | The current count of unique clients. |
| dns-queries-all-types | Number | RO | The total number of DNS queries of all types. |
| reply-unknown | Number | RO | DNS replies with an unknown status. |
| reply-nodata | Number | RO | DNS replies indicating no data. |
| reply-nxdomain | Number | RO | DNS replies indicating non-existent domain. |
| reply-cname | Number | RO | DNS replies with a CNAME record. |
| reply-ip | Number | RO | DNS replies with an IP address. |
| reply-domain | Number | RO | DNS replies with a domain name. |
| reply-rrname | Number | RO | DNS replies with a resource record name. |
| reply-servfail | Number | RO | DNS replies indicating a server failure. |
| reply-refused | Number | RO | DNS replies indicating refusal. |
| reply-notimp | Number | RO | DNS replies indicating not implemented. |
| reply-other | Number | RO | DNS replies with other statuses. |
| reply-dnssec | Number | RO | DNS replies with DNSSEC information. |
| reply-none | Number | RO | DNS replies with no data. |
| reply-blob | Number | RO | DNS replies with a BLOB (binary large object). |
| dns-queries-all-replies | Number | RO | The total number of DNS queries with all reply types. |
| privacy-level | Number | RO | The privacy level setting. |
| enabled | Switch | RO | The current status of blocking |
| disable-enable | String | RW | Is blocking enabled/disabled |
## Full Example
### Thing Configuration
```java
Thing pihole:server:a4a077edb8 "Pi-hole" @ "Location"
[
refreshIntervalSeconds=600,
hostname="http://123.456.7.89",
token="as654gadf3h1dsfh654dfh6fh7et654asd3g21fh654eth8t4swd4g3s1g65sfg5"
] {
Channels:
Type number : domains_being_blocked "Domains Blocked" [ ]
Type number : dns_queries_today "DNS Queries Today" [ ]
Type number : ads_blocked_today "Ads Blocked Today" [ ]
Type number : ads_percentage_today "Ads Percentage Today" [ ]
Type number : unique_domains "Unique Domains" [ ]
Type number : queries_forwarded "Queries Forwarded" [ ]
Type number : queries_cached "Queries Cached" [ ]
Type number : clients_ever_seen "Clients Ever Seen" [ ]
Type number : unique_clients "Unique Clients" [ ]
Type number : dns_queries_all_types "DNS Queries (All Types)" [ ]
Type number : reply_UNKNOWN "Reply UNKNOWN" [ ]
Type number : reply_NODATA "Reply NODATA" [ ]
Type number : reply_NXDOMAIN "Reply NXDOMAIN" [ ]
Type number : reply_CNAME "Reply CNAME" [ ]
Type number : reply_IP "Reply IP" [ ]
Type number : reply_DOMAIN "Reply DOMAIN" [ ]
Type number : reply_RRNAME "Reply RRNAME" [ ]
Type number : reply_SERVFAIL "Reply SERVFAIL" [ ]
Type number : reply_REFUSED "Reply REFUSED" [ ]
Type number : reply_NOTIMP "Reply NOTIMP" [ ]
Type number : reply_OTHER "Reply OTHER" [ ]
Type number : reply_DNSSEC "Reply DNSSEC" [ ]
Type number : reply_NONE "Reply NONE" [ ]
Type number : reply_BLOB "Reply BLOB" [ ]
Type number : dns_queries_all_replies "DNS Queries (All Replies)" [ ]
Type number : privacy_level "Privacy Level" [ ]
Type switch : enabled "Status" [ ]
Type string : disable-enable "Disable Blocking" [ ]
}
```
### Item Configuration
```java
Number domains_being_blocked "Domains Blocked" { channel="pihole:server:a4a077edb8:domains_being_blocked" }
Number dns_queries_today "DNS Queries Today" { channel="pihole:server:a4a077edb8:dns_queries_today" }
Number ads_blocked_today "Ads Blocked Today" { channel="pihole:server:a4a077edb8:ads_blocked_today" }
Number ads_percentage_today "Ads Percentage Today" { channel="pihole:server:a4a077edb8:ads_percentage_today" }
Number unique_domains "Unique Domains" { channel="pihole:server:a4a077edb8:unique_domains" }
Number queries_forwarded "Queries Forwarded" { channel="pihole:server:a4a077edb8:queries_forwarded" }
Number queries_cached "Queries Cached" { channel="pihole:server:a4a077edb8:queries_cached" }
Number clients_ever_seen "Clients Ever Seen" { channel="pihole:server:a4a077edb8:clients_ever_seen" }
Number unique_clients "Unique Clients" { channel="pihole:server:a4a077edb8:unique_clients" }
Number dns_queries_all_types "DNS Queries (All Types)" { channel="pihole:server:a4a077edb8:dns_queries_all_types" }
Number reply_UNKNOWN "Reply UNKNOWN" { channel="pihole:server:a4a077edb8:reply_UNKNOWN" }
Number reply_NODATA "Reply NODATA" { channel="pihole:server:a4a077edb8:reply_NODATA" }
Number reply_NXDOMAIN "Reply NXDOMAIN" { channel="pihole:server:a4a077edb8:reply_NXDOMAIN" }
Number reply_CNAME "Reply CNAME" { channel="pihole:server:a4a077edb8:reply_CNAME" }
Number reply_IP "Reply IP" { channel="pihole:server:a4a077edb8:reply_IP" }
Number reply_DOMAIN "Reply DOMAIN" { channel="pihole:server:a4a077edb8:reply_DOMAIN" }
Number reply_RRNAME "Reply RRNAME" { channel="pihole:server:a4a077edb8:reply_RRNAME" }
Number reply_SERVFAIL "Reply SERVFAIL" { channel="pihole:server:a4a077edb8:reply_SERVFAIL" }
Number reply_REFUSED "Reply REFUSED" { channel="pihole:server:a4a077edb8:reply_REFUSED" }
Number reply_NOTIMP "Reply NOTIMP" { channel="pihole:server:a4a077edb8:reply_NOTIMP" }
Number reply_OTHER "Reply OTHER" { channel="pihole:server:a4a077edb8:reply_OTHER" }
Number reply_DNSSEC "Reply DNSSEC" { channel="pihole:server:a4a077edb8:reply_DNSSEC" }
Number reply_NONE "Reply NONE" { channel="pihole:server:a4a077edb8:reply_NONE" }
Number reply_BLOB "Reply BLOB" { channel="pihole:server:a4a077edb8:reply_BLOB" }
Number dns_queries_all_replies "DNS Queries (All Replies)" { channel="pihole:server:a4a077edb8:dns_queries_all_replies" }
Number privacy_level "Privacy Level" { channel="pihole:server:a4a077edb8:privacy_level" }
Switch enabled "Status" { channel="pihole:server:a4a077edb8:enabled" }
String disable_enable "Disable Blocking" { channel="pihole:server:a4a077edb8:disable-enable" }
```
### Actions
Pi-hole binding provides actions to use in rules:
```java
import java.util.concurrent.TimeUnit
rule "test"
when
/* when */
then
val actions = getActions("pihole", "pihole:server:as8af03m38")
if (actions !== null) {
// disable blocking for 5 * 60 seconds (5 minutes)
actions.disableBlocking(5 * 60)
// disable blocking for 5 minutes
actions.disableBlocking(5, TimeUnit.MINUTES)
// disable blocking for infinity
actions.disableBlocking(0)
actions.disableBlocking()
// enable blocking
actions.enableBlocking()
}
end
```

View File

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
<version>4.3.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.binding.pihole</artifactId>
<name>openHAB Add-ons :: Bundles :: Pi-hole Binding</name>
<dependencies>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.25.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.11.0</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.pihole-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
<feature name="openhab-binding-pihole" description="Pi-hole Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.pihole/${project.version}</bundle>
</feature>
</features>

View File

@ -0,0 +1,104 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.pihole.internal;
import static java.util.Objects.requireNonNull;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.BINDING_ID;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.automation.annotation.ActionInput;
import org.openhab.core.automation.annotation.RuleAction;
import org.openhab.core.thing.binding.ThingActions;
import org.openhab.core.thing.binding.ThingActionsScope;
import org.openhab.core.thing.binding.ThingHandler;
/**
* @author Martin Grzeslowski - Initial contribution
*/
@ThingActionsScope(name = BINDING_ID)
@NonNullByDefault
public class PiHoleActions implements ThingActions {
private @Nullable PiHoleHandler handler;
@Override
public void setThingHandler(@Nullable ThingHandler handler) {
this.handler = (PiHoleHandler) handler;
}
@Override
public @Nullable ThingHandler getThingHandler() {
return handler;
}
@RuleAction(label = "@text/action.disable.label", description = "@text/action.disable.description")
public void disableBlocking(
@ActionInput(name = "time", label = "@text/action.disable.timeLabel", description = "@text/action.disable.timeDescription") long time,
@ActionInput(name = "timeUnit", label = "@text/action.disable.timeUnitLabel", description = "@text/action.disable.timeUnitDescription") @Nullable TimeUnit timeUnit)
throws PiHoleException {
if (time < 0) {
return;
}
if (timeUnit == null) {
timeUnit = SECONDS;
}
var local = handler;
if (local == null) {
return;
}
local.disableBlocking(timeUnit.toSeconds(time));
}
public static void disableBlocking(@Nullable ThingActions actions, long time, @Nullable TimeUnit timeUnit)
throws PiHoleException {
((PiHoleActions) requireNonNull(actions)).disableBlocking(time, timeUnit);
}
@RuleAction(label = "@text/action.disable.label", description = "@text/action.disable.description")
public void disableBlocking(
@ActionInput(name = "time", label = "@text/action.disable.timeLabel", description = "@text/action.disable.timeDescription") long time)
throws PiHoleException {
disableBlocking(time, null);
}
public static void disableBlocking(@Nullable ThingActions actions, long time) throws PiHoleException {
((PiHoleActions) requireNonNull(actions)).disableBlocking(time);
}
@RuleAction(label = "@text/action.disableInf.label", description = "@text/action.disableInf.description")
public void disableBlocking() throws PiHoleException {
disableBlocking(0, null);
}
public static void disableBlocking(@Nullable ThingActions actions) throws PiHoleException {
((PiHoleActions) requireNonNull(actions)).disableBlocking(0);
}
@RuleAction(label = "@text/action.enable.label", description = "@text/action.enable.description")
public void enableBlocking() throws PiHoleException {
var local = handler;
if (local == null) {
return;
}
local.enableBlocking();
}
public static void enableBlocking(@Nullable ThingActions actions) throws PiHoleException {
((PiHoleActions) requireNonNull(actions)).enableBlocking();
}
}

View File

@ -0,0 +1,70 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.pihole.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link PiHoleBindingConstants} class defines common constants, which are
* used across the whole binding.
*
* @author Martin Grzeslowski - Initial contribution
*/
@NonNullByDefault
public class PiHoleBindingConstants {
public static final String BINDING_ID = "pihole";
// List of all Thing Type UIDs
public static final ThingTypeUID PI_HOLE_TYPE = new ThingTypeUID(BINDING_ID, "server");
public static final class Channels {
public static final String DOMAINS_BEING_BLOCKED_CHANNEL = "domains-being-blocked";
public static final String DNS_QUERIES_TODAY_CHANNEL = "dns-queries-today";
public static final String ADS_BLOCKED_TODAY_CHANNEL = "ads-blocked-today";
public static final String ADS_PERCENTAGE_TODAY_CHANNEL = "ads-percentage-today";
public static final String UNIQUE_DOMAINS_CHANNEL = "unique-domains";
public static final String QUERIES_FORWARDED_CHANNEL = "queries-forwarded";
public static final String QUERIES_CACHED_CHANNEL = "queries-cached";
public static final String CLIENTS_EVER_SEEN_CHANNEL = "clients-ever-seen";
public static final String UNIQUE_CLIENTS_CHANNEL = "unique-clients";
public static final String DNS_QUERIES_ALL_TYPES_CHANNEL = "dns-queries-all-types";
public static final String REPLY_UNKNOWN_CHANNEL = "reply-unknown";
public static final String REPLY_NODATA_CHANNEL = "reply-nodata";
public static final String REPLY_NXDOMAIN_CHANNEL = "reply-nxdomain";
public static final String REPLY_CNAME_CHANNEL = "reply-cname";
public static final String REPLY_IP_CHANNEL = "reply-ip";
public static final String REPLY_DOMAIN_CHANNEL = "reply-domain";
public static final String REPLY_RRNAME_CHANNEL = "reply-rrname";
public static final String REPLY_SERVFAIL_CHANNEL = "reply-servfail";
public static final String REPLY_REFUSED_CHANNEL = "reply-refused";
public static final String REPLY_NOTIMP_CHANNEL = "reply-notimp";
public static final String REPLY_OTHER_CHANNEL = "reply-other";
public static final String REPLY_DNSSEC_CHANNEL = "reply-dnssec";
public static final String REPLY_NONE_CHANNEL = "reply-none";
public static final String REPLY_BLOB_CHANNEL = "reply-blob";
public static final String DNS_QUERIES_ALL_REPLIES_CHANNEL = "dns-queries-all-replies";
public static final String PRIVACY_LEVEL_CHANNEL = "privacy-level";
public static final String ENABLED_CHANNEL = "enabled";
public static final String DISABLE_ENABLE_CHANNEL = "disable-enable";
public static enum DisableEnable {
DISABLE,
FOR_10_SEC,
FOR_30_SEC,
FOR_5_MIN,
ENABLE
}
}
}

View File

@ -0,0 +1,27 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.pihole.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link PiHoleConfiguration} class contains fields mapping thing configuration parameters.
*
* @author Martin Grzeslowski - Initial contribution
*/
@NonNullByDefault
public class PiHoleConfiguration {
public String hostname = "";
public String token = "";
public int refreshIntervalSeconds = 600;
}

View File

@ -0,0 +1,34 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.pihole.internal;
import java.io.Serial;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* @author Martin Grzeslowski - Initial contribution
*/
@NonNullByDefault
public class PiHoleException extends Exception {
@Serial
private static final long serialVersionUID = 1L;
public PiHoleException(String message) {
super(message);
}
public PiHoleException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -0,0 +1,282 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.pihole.internal;
import static java.util.concurrent.TimeUnit.*;
import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.ADS_BLOCKED_TODAY_CHANNEL;
import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.ADS_PERCENTAGE_TODAY_CHANNEL;
import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.CLIENTS_EVER_SEEN_CHANNEL;
import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.DISABLE_ENABLE_CHANNEL;
import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.DNS_QUERIES_ALL_REPLIES_CHANNEL;
import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.DNS_QUERIES_ALL_TYPES_CHANNEL;
import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.DNS_QUERIES_TODAY_CHANNEL;
import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.DOMAINS_BEING_BLOCKED_CHANNEL;
import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.DisableEnable;
import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.DisableEnable.ENABLE;
import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.ENABLED_CHANNEL;
import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.PRIVACY_LEVEL_CHANNEL;
import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.QUERIES_CACHED_CHANNEL;
import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.QUERIES_FORWARDED_CHANNEL;
import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.REPLY_BLOB_CHANNEL;
import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.REPLY_CNAME_CHANNEL;
import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.REPLY_DNSSEC_CHANNEL;
import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.REPLY_DOMAIN_CHANNEL;
import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.REPLY_IP_CHANNEL;
import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.REPLY_NODATA_CHANNEL;
import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.REPLY_NONE_CHANNEL;
import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.REPLY_NOTIMP_CHANNEL;
import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.REPLY_NXDOMAIN_CHANNEL;
import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.REPLY_OTHER_CHANNEL;
import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.REPLY_REFUSED_CHANNEL;
import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.REPLY_RRNAME_CHANNEL;
import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.REPLY_SERVFAIL_CHANNEL;
import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.REPLY_UNKNOWN_CHANNEL;
import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.UNIQUE_CLIENTS_CHANNEL;
import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.UNIQUE_DOMAINS_CHANNEL;
import static org.openhab.core.library.unit.Units.PERCENT;
import static org.openhab.core.thing.ThingStatus.OFFLINE;
import static org.openhab.core.thing.ThingStatus.ONLINE;
import static org.openhab.core.thing.ThingStatus.UNKNOWN;
import static org.openhab.core.thing.ThingStatusDetail.*;
import java.math.BigDecimal;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Collection;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ScheduledFuture;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.pihole.internal.rest.AdminService;
import org.openhab.binding.pihole.internal.rest.JettyAdminService;
import org.openhab.binding.pihole.internal.rest.model.DnsStatistics;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link PiHoleHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Martin Grzeslowski - Initial contribution
*/
@NonNullByDefault
public class PiHoleHandler extends BaseThingHandler implements AdminService {
private static final int HTTP_DELAY_SECONDS = 1;
private final Logger logger = LoggerFactory.getLogger(PiHoleHandler.class);
private final Object lock = new Object();
private final HttpClient httpClient;
private @Nullable AdminService adminService;
private @Nullable DnsStatistics dnsStatistics;
private @Nullable ScheduledFuture<?> scheduledFuture;
public PiHoleHandler(Thing thing, HttpClient httpClient) {
super(thing);
this.httpClient = httpClient;
}
@Override
public void initialize() {
// set the thing status to UNKNOWN temporarily and let the background task decide for the real status.
// the framework is then able to reuse the resources from the thing handler initialization.
// we set this upfront to reliably check status updates in unit tests.
updateStatus(UNKNOWN);
var config = getConfigAs(PiHoleConfiguration.class);
if (config.refreshIntervalSeconds <= 0) {
updateStatus(OFFLINE, CONFIGURATION_ERROR, "@text/handler.init.wrongInterval");
return;
}
URI hostname;
try {
hostname = new URI(config.hostname);
} catch (URISyntaxException e) {
updateStatus(OFFLINE, CONFIGURATION_ERROR,
"@token/handler.init.invalidHostname[\"" + config.hostname + "\"]");
return;
}
if (config.token.isEmpty()) {
updateStatus(OFFLINE, CONFIGURATION_ERROR, "@token/handler.init.noToken");
return;
}
adminService = new JettyAdminService(config.token, hostname, httpClient);
scheduledFuture = scheduler.scheduleWithFixedDelay(this::update, 0, config.refreshIntervalSeconds, SECONDS);
// do not set status here, the background task will do it.
}
private void update() {
var local = adminService;
if (local == null) {
return;
}
// this block can be called from at least 2 threads
// check disableBlocking method
synchronized (lock) {
try {
logger.debug("Refreshing DnsStatistics from Pi-hole");
local.summary().ifPresent(statistics -> dnsStatistics = statistics);
refresh();
updateStatus(ONLINE);
} catch (Exception e) {
logger.debug("Error occurred when refreshing DnsStatistics from Pi-hole", e);
updateStatus(OFFLINE, COMMUNICATION_ERROR, e.getLocalizedMessage());
}
}
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (command instanceof RefreshType) {
refresh();
return;
}
if (DISABLE_ENABLE_CHANNEL.equals(channelUID.getId())) {
if (command instanceof StringType stringType) {
var value = DisableEnable.valueOf(stringType.toString());
try {
switch (value) {
case DISABLE -> disableBlocking(0);
case FOR_10_SEC -> disableBlocking(10);
case FOR_30_SEC -> disableBlocking(30);
case FOR_5_MIN -> disableBlocking(MINUTES.toSeconds(5));
case ENABLE -> enableBlocking();
}
} catch (PiHoleException ex) {
logger.debug("Cannot invoke {} on channel {}", value, channelUID, ex);
updateStatus(OFFLINE, COMMUNICATION_ERROR, ex.getLocalizedMessage());
}
}
}
}
private void refresh() {
var localDnsStatistics = dnsStatistics;
if (localDnsStatistics == null) {
return;
}
updateDecimalState(DOMAINS_BEING_BLOCKED_CHANNEL, localDnsStatistics.domainsBeingBlocked());
updateDecimalState(DNS_QUERIES_TODAY_CHANNEL, localDnsStatistics.dnsQueriesToday());
updateDecimalState(ADS_BLOCKED_TODAY_CHANNEL, localDnsStatistics.adsBlockedToday());
updateDecimalState(UNIQUE_DOMAINS_CHANNEL, localDnsStatistics.uniqueDomains());
updateDecimalState(QUERIES_FORWARDED_CHANNEL, localDnsStatistics.queriesForwarded());
updateDecimalState(QUERIES_CACHED_CHANNEL, localDnsStatistics.queriesCached());
updateDecimalState(CLIENTS_EVER_SEEN_CHANNEL, localDnsStatistics.clientsEverSeen());
updateDecimalState(UNIQUE_CLIENTS_CHANNEL, localDnsStatistics.uniqueClients());
updateDecimalState(DNS_QUERIES_ALL_TYPES_CHANNEL, localDnsStatistics.dnsQueriesAllTypes());
updateDecimalState(REPLY_UNKNOWN_CHANNEL, localDnsStatistics.replyUnknown());
updateDecimalState(REPLY_NODATA_CHANNEL, localDnsStatistics.replyNoData());
updateDecimalState(REPLY_NXDOMAIN_CHANNEL, localDnsStatistics.replyNXDomain());
updateDecimalState(REPLY_CNAME_CHANNEL, localDnsStatistics.replyCName());
updateDecimalState(REPLY_IP_CHANNEL, localDnsStatistics.replyIP());
updateDecimalState(REPLY_DOMAIN_CHANNEL, localDnsStatistics.replyDomain());
updateDecimalState(REPLY_RRNAME_CHANNEL, localDnsStatistics.replyRRName());
updateDecimalState(REPLY_SERVFAIL_CHANNEL, localDnsStatistics.replyServFail());
updateDecimalState(REPLY_REFUSED_CHANNEL, localDnsStatistics.replyRefused());
updateDecimalState(REPLY_NOTIMP_CHANNEL, localDnsStatistics.replyNotImp());
updateDecimalState(REPLY_OTHER_CHANNEL, localDnsStatistics.replyOther());
updateDecimalState(REPLY_DNSSEC_CHANNEL, localDnsStatistics.replyDNSSEC());
updateDecimalState(REPLY_NONE_CHANNEL, localDnsStatistics.replyNone());
updateDecimalState(REPLY_BLOB_CHANNEL, localDnsStatistics.replyBlob());
updateDecimalState(DNS_QUERIES_ALL_REPLIES_CHANNEL, localDnsStatistics.dnsQueriesAllTypes());
updateDecimalState(PRIVACY_LEVEL_CHANNEL, localDnsStatistics.privacyLevel());
var adsPercentageToday = localDnsStatistics.adsPercentageToday();
if (adsPercentageToday != null) {
var state = new QuantityType<>(new BigDecimal(adsPercentageToday.toString()), PERCENT);
updateState(ADS_PERCENTAGE_TODAY_CHANNEL, state);
}
updateState(ENABLED_CHANNEL, OnOffType.from(localDnsStatistics.enabled()));
if (localDnsStatistics.enabled()) {
updateState(DISABLE_ENABLE_CHANNEL, new StringType(ENABLE.toString()));
}
}
private void updateDecimalState(String channelID, @Nullable Integer value) {
if (value == null) {
return;
}
updateState(channelID, new DecimalType(value));
}
@Override
public Collection<Class<? extends ThingHandlerService>> getServices() {
return Set.of(PiHoleActions.class);
}
@Override
public void dispose() {
adminService = null;
dnsStatistics = null;
var localScheduledFuture = scheduledFuture;
if (localScheduledFuture != null) {
localScheduledFuture.cancel(true);
scheduledFuture = null;
}
super.dispose();
}
@Override
public Optional<DnsStatistics> summary() throws PiHoleException {
var local = adminService;
if (local == null) {
throw new IllegalStateException("AdminService not initialized");
}
return local.summary();
}
@Override
public void disableBlocking(long seconds) throws PiHoleException {
var local = adminService;
if (local == null) {
throw new IllegalStateException("AdminService not initialized");
}
local.disableBlocking(seconds);
// update the summary to get the value of DISABLED_CHANNEL channel
scheduler.schedule(this::update, HTTP_DELAY_SECONDS, SECONDS);
if (seconds > 0) {
// update the summary to get the value of ENABLED_CHANNEL channel
// after the X seconds it probably will be true again
scheduler.schedule(this::update, seconds + HTTP_DELAY_SECONDS, SECONDS);
}
}
@Override
public void enableBlocking() throws PiHoleException {
var local = adminService;
if (local == null) {
throw new IllegalStateException("AdminService not initialized");
}
local.enableBlocking();
// update the summary to get the value of DISABLED_CHANNEL channel
scheduler.schedule(this::update, HTTP_DELAY_SECONDS, SECONDS);
}
}

View File

@ -0,0 +1,64 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.pihole.internal;
import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.PI_HOLE_TYPE;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* The {@link PiHoleHandlerFactory} is responsible for creating things and thing
* handlers.
*
* @author Martin Grzeslowski - Initial contribution
*/
@NonNullByDefault
@Component(configurationPid = "binding.pihole", service = ThingHandlerFactory.class)
public class PiHoleHandlerFactory extends BaseThingHandlerFactory {
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(PI_HOLE_TYPE);
private final HttpClientFactory httpClientFactory;
@Activate
public PiHoleHandlerFactory(@Reference HttpClientFactory httpClientFactory) {
this.httpClientFactory = httpClientFactory;
}
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
}
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (PI_HOLE_TYPE.equals(thingTypeUID)) {
return new PiHoleHandler(thing, httpClientFactory.getCommonHttpClient());
}
return null;
}
}

View File

@ -0,0 +1,48 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.pihole.internal.rest;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.pihole.internal.PiHoleException;
import org.openhab.binding.pihole.internal.rest.model.DnsStatistics;
/**
* @author Martin Grzeslowski - Initial contribution
*/
@NonNullByDefault
public interface AdminService {
/**
* Retrieves a summary of DNS statistics.
*
* @return An optional containing the DNS statistics.
* @throws PiHoleException In case of error
*/
Optional<DnsStatistics> summary() throws PiHoleException;
/**
* Disables blocking for a specified duration.
*
* @param seconds The duration in seconds for which blocking should be disabled.
* @throws PiHoleException In case of error
*/
void disableBlocking(long seconds) throws PiHoleException;
/**
* Enables blocking.
*
* @throws PiHoleException In case of error
*/
void enableBlocking() throws PiHoleException;
}

View File

@ -0,0 +1,88 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.pihole.internal.rest;
import static java.util.concurrent.TimeUnit.SECONDS;
import java.net.URI;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
import org.openhab.binding.pihole.internal.PiHoleException;
import org.openhab.binding.pihole.internal.rest.model.DnsStatistics;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.FieldNamingPolicy;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
/**
* @author Martin Grzeslowski - Initial contribution
*/
@NonNullByDefault
public class JettyAdminService implements AdminService {
private static final Gson GSON = new GsonBuilder()
.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create();
private static final long TIMEOUT_SECONDS = 10L;
private final Logger logger = LoggerFactory.getLogger(JettyAdminService.class);
private final String token;
private final URI baseUrl;
private final HttpClient client;
public JettyAdminService(String token, URI baseUrl, HttpClient client) {
this.token = token;
this.baseUrl = baseUrl;
this.client = client;
}
@Override
public Optional<DnsStatistics> summary() throws PiHoleException {
logger.debug("Getting summary");
var url = baseUrl.resolve("/admin/api.php?summaryRaw&auth=" + token);
var request = client.newRequest(url).timeout(TIMEOUT_SECONDS, SECONDS);
var response = send(request);
var content = response.getContentAsString();
return Optional.ofNullable(GSON.fromJson(content, DnsStatistics.class));
}
private static ContentResponse send(Request request) throws PiHoleException {
try {
return request.send();
} catch (InterruptedException | TimeoutException | ExecutionException e) {
throw new PiHoleException(
"Exception while sending request to Pi-hole. %s".formatted(e.getLocalizedMessage()), e);
}
}
@Override
public void disableBlocking(long seconds) throws PiHoleException {
logger.debug("Disabling blocking for {} seconds", seconds);
var url = baseUrl.resolve("/admin/api.php?disable=%s&auth=%s".formatted(seconds, token));
var request = client.newRequest(url).timeout(TIMEOUT_SECONDS, SECONDS);
send(request);
}
@Override
public void enableBlocking() throws PiHoleException {
logger.debug("Enabling blocking");
var url = baseUrl.resolve("/admin/api.php?disable&auth=%s".formatted(token));
var request = client.newRequest(url).timeout(TIMEOUT_SECONDS, SECONDS);
send(request);
}
}

View File

@ -0,0 +1,46 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.pihole.internal.rest.model;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
/**
* @author Martin Grzeslowski - Initial contribution
*/
@NonNullByDefault
public record DnsStatistics(@Nullable Integer domainsBeingBlocked, @Nullable Integer dnsQueriesToday,
@Nullable Integer adsBlockedToday, @Nullable Double adsPercentageToday, @Nullable Integer uniqueDomains,
@Nullable Integer queriesForwarded, @Nullable Integer queriesCached, @Nullable Integer clientsEverSeen,
@Nullable Integer uniqueClients, @Nullable Integer dnsQueriesAllTypes,
@SerializedName("reply_UNKNOWN") @Nullable Integer replyUnknown,
@SerializedName("reply_NODATA") @Nullable Integer replyNoData,
@SerializedName("reply_NXDOMAIN") @Nullable Integer replyNXDomain,
@SerializedName("reply_CNAME") @Nullable Integer replyCName,
@SerializedName("reply_IP") @Nullable Integer replyIP,
@SerializedName("reply_DOMAIN") @Nullable Integer replyDomain,
@SerializedName("reply_RRNAME") @Nullable Integer replyRRName,
@SerializedName("reply_SERVFAIL") @Nullable Integer replyServFail,
@SerializedName("reply_REFUSED") @Nullable Integer replyRefused,
@SerializedName("reply_NOTIMP") @Nullable Integer replyNotImp,
@SerializedName("reply_OTHER") @Nullable Integer replyOther,
@SerializedName("reply_DNSSEC") @Nullable Integer replyDNSSEC,
@SerializedName("reply_NONE") @Nullable Integer replyNone,
@SerializedName("reply_BLOB") @Nullable Integer replyBlob, @Nullable Integer dnsQueriesAllReplies,
@Nullable Integer privacyLevel, @Nullable String status, @Nullable GravityLastUpdated gravityLastUpdated) {
public boolean enabled() {
return "enabled".equalsIgnoreCase(status);
}
}

View File

@ -0,0 +1,23 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.pihole.internal.rest.model;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* @author Martin Grzeslowski - Initial contribution
*/
@NonNullByDefault
public record GravityLastUpdated(@Nullable Boolean fileExists, @Nullable Long absolute, @Nullable Relative relative) {
}

View File

@ -0,0 +1,23 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.pihole.internal.rest.model;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* @author Martin Grzeslowski - Initial contribution
*/
@NonNullByDefault
public record Relative(@Nullable Integer days, @Nullable Integer hours, @Nullable Integer minutes) {
}

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<addon:addon id="pihole" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:addon="https://openhab.org/schemas/addon/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/addon/v1.0.0 https://openhab.org/schemas/addon-1.0.0.xsd">
<type>binding</type>
<name>Pi-hole Binding</name>
<description>This is the binding for Pi-hole.</description>
<connection>cloud</connection>
</addon:addon>

View File

@ -0,0 +1,167 @@
# add-on
addon.pihole.name = Pi-hole Binding
addon.pihole.description = This is the binding for Pi-hole.
# thing types
thing-type.pihole.server.label = Pi-hole Server
thing-type.pihole.server.description = This thing represents a Pi-hole server and is used for the Pi-hole binding.
thing-type.pihole.server.channel.ads-blocked-today.label = Ads Blocked Today
thing-type.pihole.server.channel.ads-blocked-today.description = The number of ads blocked today.
thing-type.pihole.server.channel.ads-percentage-today.label = Ads Percentage Today
thing-type.pihole.server.channel.ads-percentage-today.description = The percentage of ads blocked today.
thing-type.pihole.server.channel.clients-ever-seen.label = Clients Ever Seen
thing-type.pihole.server.channel.clients-ever-seen.description = The total number of unique clients ever seen.
thing-type.pihole.server.channel.dns-queries-all-replies.label = DNS Queries (All Replies)
thing-type.pihole.server.channel.dns-queries-all-replies.description = The total number of DNS queries with all reply types.
thing-type.pihole.server.channel.dns-queries-all-types.label = DNS Queries (All Types)
thing-type.pihole.server.channel.dns-queries-all-types.description = The total number of DNS queries of all types.
thing-type.pihole.server.channel.dns-queries-today.label = DNS Queries Today
thing-type.pihole.server.channel.dns-queries-today.description = The count of DNS queries made today.
thing-type.pihole.server.channel.domains-being-blocked.label = Domains Blocked
thing-type.pihole.server.channel.domains-being-blocked.description = The total number of domains currently being blocked.
thing-type.pihole.server.channel.privacy-level.label = Privacy Level
thing-type.pihole.server.channel.privacy-level.description = The privacy level setting.
thing-type.pihole.server.channel.queries-cached.label = Queries Cached
thing-type.pihole.server.channel.queries-cached.description = The number of queries served from the cache.
thing-type.pihole.server.channel.queries-forwarded.label = Queries Forwarded
thing-type.pihole.server.channel.queries-forwarded.description = The number of queries forwarded to an external DNS server.
thing-type.pihole.server.channel.reply-blob.label = Reply BLOB
thing-type.pihole.server.channel.reply-blob.description = DNS replies with a BLOB (binary large object).
thing-type.pihole.server.channel.reply-cname.label = Reply CNAME
thing-type.pihole.server.channel.reply-cname.description = DNS replies with a CNAME record.
thing-type.pihole.server.channel.reply-dnssec.label = Reply DNSSEC
thing-type.pihole.server.channel.reply-dnssec.description = DNS replies with DNSSEC information.
thing-type.pihole.server.channel.reply-domain.label = Reply DOMAIN
thing-type.pihole.server.channel.reply-domain.description = DNS replies with a domain name.
thing-type.pihole.server.channel.reply-ip.label = Reply IP
thing-type.pihole.server.channel.reply-ip.description = DNS replies with an IP address.
thing-type.pihole.server.channel.reply-nodata.label = Reply NODATA
thing-type.pihole.server.channel.reply-nodata.description = DNS replies indicating no data.
thing-type.pihole.server.channel.reply-none.label = Reply NONE
thing-type.pihole.server.channel.reply-none.description = DNS replies with no data.
thing-type.pihole.server.channel.reply-notimp.label = Reply NOTIMP
thing-type.pihole.server.channel.reply-notimp.description = DNS replies indicating not implemented.
thing-type.pihole.server.channel.reply-nxdomain.label = Reply NXDOMAIN
thing-type.pihole.server.channel.reply-nxdomain.description = DNS replies indicating non-existent domain.
thing-type.pihole.server.channel.reply-other.label = Reply OTHER
thing-type.pihole.server.channel.reply-other.description = DNS replies with other statuses.
thing-type.pihole.server.channel.reply-refused.label = Reply REFUSED
thing-type.pihole.server.channel.reply-refused.description = DNS replies indicating refusal.
thing-type.pihole.server.channel.reply-rrname.label = Reply RRNAME
thing-type.pihole.server.channel.reply-rrname.description = DNS replies with a resource record name.
thing-type.pihole.server.channel.reply-servfail.label = Reply SERVFAIL
thing-type.pihole.server.channel.reply-servfail.description = DNS replies indicating a server failure.
thing-type.pihole.server.channel.reply-unknown.label = Reply UNKNOWN
thing-type.pihole.server.channel.reply-unknown.description = DNS replies with an unknown status.
thing-type.pihole.server.channel.unique-clients.label = Unique Clients
thing-type.pihole.server.channel.unique-clients.description = The current count of unique clients.
thing-type.pihole.server.channel.unique-domains.label = Unique Domains
thing-type.pihole.server.channel.unique-domains.description = The count of unique domains queried.
# thing types config
thing-type.config.pihole.server.hostname.label = Hostname
thing-type.config.pihole.server.hostname.description = Hostname or IP address of the device
thing-type.config.pihole.server.refreshIntervalSeconds.label = Refresh Interval
thing-type.config.pihole.server.refreshIntervalSeconds.description = Interval the device is polled in sec.
thing-type.config.pihole.server.token.label = Token
thing-type.config.pihole.server.token.description = Token to access the device. To generate token go to `settings` > `API` > `Show API token`
# channel types
channel-type.pihole.disable-enable-channel.label = Disable Blocking
channel-type.pihole.disable-enable-channel.command.option.DISABLE = Disable Blocking Indefinitely
channel-type.pihole.disable-enable-channel.command.option.FOR_10_SEC = Disable Blocking for 10 seconds
channel-type.pihole.disable-enable-channel.command.option.FOR_30_SEC = Disable Blocking for 30 seconds
channel-type.pihole.disable-enable-channel.command.option.FOR_5_MIN = Disable Blocking for 5 minutes
channel-type.pihole.disable-enable-channel.command.option.ENABLE = Enable Blocking
channel-type.pihole.enabled-channel.label = Status
channel-type.pihole.enabled-channel.description = The current status of blocking
channel-type.pihole.number-channel.label = Number channel
# channel types
channel.ads_blocked_today.label = Ads Blocked Today
channel.ads_blocked_today.description = The number of ads blocked today.
channel.ads_percentage_today.label = Ads Percentage Today
channel.ads_percentage_today.description = The percentage of ads blocked today.
channel.clients_ever_seen.label = Clients Ever Seen
channel.clients_ever_seen.description = The total number of unique clients ever seen.
channel.disable-enable.label = Disable Blocking
channel.disable-enable.description = Commands to disable or enable blocking.
channel.disable-enable.command.DISABLE = Disable Blocking Indefinitely
channel.disable-enable.command.FOR_10_SEC = Disable Blocking for 10 seconds
channel.disable-enable.command.FOR_30_SEC = Disable Blocking for 30 seconds
channel.disable-enable.command.FOR_5_MIN = Disable Blocking for 5 minutes
channel.disable-enable.command.ENABLE = Enable Blocking
channel.dns_queries_all_replies.label = DNS Queries (All Replies)
channel.dns_queries_all_replies.description = The total number of DNS queries with all reply types.
channel.dns_queries_all_types.label = DNS Queries (All Types)
channel.dns_queries_all_types.description = The total number of DNS queries of all types.
channel.dns_queries_today.label = DNS Queries Today
channel.dns_queries_today.description = The count of DNS queries made today.
channel.domains_being_blocked.label = Domains Blocked
channel.domains_being_blocked.description = The total number of domains currently being blocked.
channel.enabled.label = Enabled
channel.enabled.description = The current status of blocking.
channel.privacy_level.label = Privacy Level
channel.privacy_level.description = The privacy level setting.
channel.queries_cached.label = Queries Cached
channel.queries_cached.description = The number of queries served from the cache.
channel.queries_forwarded.label = Queries Forwarded
channel.queries_forwarded.description = The number of queries forwarded to an external DNS server.
channel.reply_BLOB.label = Reply BLOB
channel.reply_BLOB.description = DNS replies with a BLOB (binary large object).
channel.reply_CNAME.label = Reply CNAME
channel.reply_CNAME.description = DNS replies with a CNAME record.
channel.reply_DNSSEC.label = Reply DNSSEC
channel.reply_DNSSEC.description = DNS replies with DNSSEC information.
channel.reply_DOMAIN.label = Reply DOMAIN
channel.reply_DOMAIN.description = DNS replies with a domain name.
channel.reply_IP.label = Reply IP
channel.reply_IP.description = DNS replies with an IP address.
channel.reply_NODATA.label = Reply NODATA
channel.reply_NODATA.description = DNS replies indicating no data.
channel.reply_NONE.label = Reply NONE
channel.reply_NONE.description = DNS replies with no data.
channel.reply_NOTIMP.label = Reply NOTIMP
channel.reply_NOTIMP.description = DNS replies indicating not implemented.
channel.reply_NXDOMAIN.label = Reply NXDOMAIN
channel.reply_NXDOMAIN.description = DNS replies indicating non-existent domain.
channel.reply_OTHER.label = Reply OTHER
channel.reply_OTHER.description = DNS replies with other statuses.
channel.reply_REFUSED.label = Reply REFUSED
channel.reply_REFUSED.description = DNS replies indicating refusal.
channel.reply_RRNAME.label = Reply RRNAME
channel.reply_RRNAME.description = DNS replies with a resource record name.
channel.reply_SERVFAIL.label = Reply SERVFAIL
channel.reply_SERVFAIL.description = DNS replies indicating a server failure.
channel.reply_UNKNOWN.label = Reply UNKNOWN
channel.reply_UNKNOWN.description = DNS replies with an unknown status.
channel.unique_clients.label = Unique Clients
channel.unique_clients.description = The current count of unique clients.
channel.unique_domains.label = Unique Domains
channel.unique_domains.description = The count of unique domains queried.
thing.server.label = Pi-hole Binding Thing
thing.server.description = Sample thing for Pi-hole Binding
# action
action.disable.label = Disable blocking ads
action.disable.description = Temporarily stop blocking advertisements.
action.disableInf.label = Disable blocking ads (for infinity)
action.disableInf.description = Stop blocking advertisements.
action.disable.timeLabel = Duration
action.disable.timeDescription = Specify the time for which ad blocking should be disabled (e.g., "for 30 minutes").
action.disable.timeUnitLabel = Time Unit
action.disable.timeUnitDescription = The unit of time for the specified duration.
action.enable.label = Enable blocking ads
action.enable.description = Resume blocking advertisements.
# from code
handler.init.wrongInterval = Refresh interval needs to be greater than 0!
handler.init.noToken = Please provide token
handler.init.invalidHostname = Invalid hostname "{0}"

View File

@ -0,0 +1,164 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="pihole"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="server">
<label>Pi-hole Server</label>
<description>This thing represents a Pi-hole server and is used for the Pi-hole binding.</description>
<channels>
<channel id="domains-being-blocked" typeId="number-channel">
<label>Domains Blocked</label>
<description>The total number of domains currently being blocked.</description>
</channel>
<channel id="dns-queries-today" typeId="number-channel">
<label>DNS Queries Today</label>
<description>The count of DNS queries made today.</description>
</channel>
<channel id="ads-blocked-today" typeId="number-channel">
<label>Ads Blocked Today</label>
<description>The number of ads blocked today.</description>
</channel>
<channel id="ads-percentage-today" typeId="number-channel">
<label>Ads Percentage Today</label>
<description>The percentage of ads blocked today.</description>
</channel>
<channel id="unique-domains" typeId="number-channel">
<label>Unique Domains</label>
<description>The count of unique domains queried.</description>
</channel>
<channel id="queries-forwarded" typeId="number-channel">
<label>Queries Forwarded</label>
<description>The number of queries forwarded to an external DNS server.</description>
</channel>
<channel id="queries-cached" typeId="number-channel">
<label>Queries Cached</label>
<description>The number of queries served from the cache.</description>
</channel>
<channel id="clients-ever-seen" typeId="number-channel">
<label>Clients Ever Seen</label>
<description>The total number of unique clients ever seen.</description>
</channel>
<channel id="unique-clients" typeId="number-channel">
<label>Unique Clients</label>
<description>The current count of unique clients.</description>
</channel>
<channel id="dns-queries-all-types" typeId="number-channel">
<label>DNS Queries (All Types)</label>
<description>The total number of DNS queries of all types.</description>
</channel>
<channel id="reply-unknown" typeId="number-channel">
<label>Reply UNKNOWN</label>
<description>DNS replies with an unknown status.</description>
</channel>
<channel id="reply-nodata" typeId="number-channel">
<label>Reply NODATA</label>
<description>DNS replies indicating no data.</description>
</channel>
<channel id="reply-nxdomain" typeId="number-channel">
<label>Reply NXDOMAIN</label>
<description>DNS replies indicating non-existent domain.</description>
</channel>
<channel id="reply-cname" typeId="number-channel">
<label>Reply CNAME</label>
<description>DNS replies with a CNAME record.</description>
</channel>
<channel id="reply-ip" typeId="number-channel">
<label>Reply IP</label>
<description>DNS replies with an IP address.</description>
</channel>
<channel id="reply-domain" typeId="number-channel">
<label>Reply DOMAIN</label>
<description>DNS replies with a domain name.</description>
</channel>
<channel id="reply-rrname" typeId="number-channel">
<label>Reply RRNAME</label>
<description>DNS replies with a resource record name.</description>
</channel>
<channel id="reply-servfail" typeId="number-channel">
<label>Reply SERVFAIL</label>
<description>DNS replies indicating a server failure.</description>
</channel>
<channel id="reply-refused" typeId="number-channel">
<label>Reply REFUSED</label>
<description>DNS replies indicating refusal.</description>
</channel>
<channel id="reply-notimp" typeId="number-channel">
<label>Reply NOTIMP</label>
<description>DNS replies indicating not implemented.</description>
</channel>
<channel id="reply-other" typeId="number-channel">
<label>Reply OTHER</label>
<description>DNS replies with other statuses.</description>
</channel>
<channel id="reply-dnssec" typeId="number-channel">
<label>Reply DNSSEC</label>
<description>DNS replies with DNSSEC information.</description>
</channel>
<channel id="reply-none" typeId="number-channel">
<label>Reply NONE</label>
<description>DNS replies with no data.</description>
</channel>
<channel id="reply-blob" typeId="number-channel">
<label>Reply BLOB</label>
<description>DNS replies with a BLOB (binary large object).</description>
</channel>
<channel id="dns-queries-all-replies" typeId="number-channel">
<label>DNS Queries (All Replies)</label>
<description>The total number of DNS queries with all reply types.</description>
</channel>
<channel id="privacy-level" typeId="number-channel">
<label>Privacy Level</label>
<description>The privacy level setting.</description>
</channel>
<channel id="enabled" typeId="enabled-channel"/>
<channel id="disable-enable" typeId="disable-enable-channel"/>
</channels>
<config-description>
<parameter name="hostname" type="text" required="true">
<context>network-address</context>
<label>Hostname</label>
<description>Hostname or IP address of the device</description>
</parameter>
<parameter name="token" type="text" required="true">
<context>password</context>
<label>Token</label>
<description>Token to access the device. To generate token go to `settings` > `API` > `Show API token`</description>
</parameter>
<parameter name="refreshIntervalSeconds" type="integer" unit="s" min="1">
<label>Refresh Interval</label>
<description>Interval the device is polled in sec.</description>
<default>600</default>
<advanced>true</advanced>
</parameter>
</config-description>
</thing-type>
<channel-type id="number-channel">
<item-type>Number</item-type>
<label>Number channel</label>
<state readOnly="true"/>
</channel-type>
<channel-type id="enabled-channel">
<item-type>Switch</item-type>
<label>Status</label>
<description>The current status of blocking</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="disable-enable-channel">
<item-type>String</item-type>
<label>Disable Blocking</label>
<command>
<options>
<option value="DISABLE">Disable Blocking Indefinitely</option>
<option value="FOR_10_SEC">Disable Blocking for 10 seconds</option>
<option value="FOR_30_SEC">Disable Blocking for 30 seconds</option>
<option value="FOR_5_MIN">Disable Blocking for 5 minutes</option>
<option value="ENABLE">Enable Blocking</option>
</options>
</command>
</channel-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,130 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.pihole.internal.rest;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import java.net.URI;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.openhab.binding.pihole.internal.rest.model.DnsStatistics;
import org.openhab.binding.pihole.internal.rest.model.GravityLastUpdated;
import org.openhab.binding.pihole.internal.rest.model.Relative;
/**
* @author Martin Grzeslowski - Initial contribution
*/
@NonNullByDefault
public class JettyAdminServiceTest {
String content = """
{
"domains_being_blocked": 131355,
"dns_queries_today": 27459,
"ads_blocked_today": 2603,
"ads_percentage_today": 9.479588,
"unique_domains": 6249,
"queries_forwarded": 16030,
"queries_cached": 8525,
"clients_ever_seen": 2,
"unique_clients": 2,
"dns_queries_all_types": 27459,
"reply_UNKNOWN": 631,
"reply_NODATA": 3168,
"reply_NXDOMAIN": 492,
"reply_CNAME": 9819,
"reply_IP": 13224,
"reply_DOMAIN": 48,
"reply_RRNAME": 0,
"reply_SERVFAIL": 0,
"reply_REFUSED": 0,
"reply_NOTIMP": 0,
"reply_OTHER": 0,
"reply_DNSSEC": 0,
"reply_NONE": 0,
"reply_BLOB": 77,
"dns_queries_all_replies": 27459,
"privacy_level": 0,
"status": "enabled",
"gravity_last_updated": {
"file_exists": true,
"absolute": 1712457841,
"relative": {
"days": 0,
"hours": 7,
"minutes": 3
}
}
}
""";
// Returns a DnsStatistics object when called with valid token and baseUrl
@Test
@DisplayName("Returns a DnsStatistics object when called with valid token and baseUrl")
public void testReturnsDnsStatisticsObjectWithValidTokenAndBaseUrl() throws Exception {
// Given
var token = "validToken";
var baseUrl = URI.create("https://example.com");
var client = mock(HttpClient.class);
var adminService = new JettyAdminService(token, baseUrl, client);
var dnsStatistics = new DnsStatistics(131355, // domains_being_blocked
27459, // dns_queries_today
2603, // ads_blocked_today
9.479588, // ads_percentage_today
6249, // unique_domains
16030, // queries_forwarded
8525, // queries_cached
2, // clients_ever_seen
2, // unique_clients
27459, // dns_queries_all_types
631, // reply_UNKNOWN
3168, // reply_NODATA
492, // reply_NXDOMAIN
9819, // reply_CNAME
13224, // reply_IP
48, // reply_DOMAIN
0, // reply_RRNAME
0, // reply_SERVFAIL
0, // reply_REFUSED
0, // reply_NOTIMP
0, // reply_OTHER
0, // reply_DNSSEC
0, // reply_NONE
77, // reply_BLOB
27459, // dns_queries_all_replies
0, // privacy_level
"enabled", // status
new GravityLastUpdated(true, 1712457841L, new Relative(0, 7, 3)));
var response = mock(ContentResponse.class);
var request = mock(Request.class);
given(request.timeout(10, SECONDS)).willReturn(request);
given(client.newRequest(URI.create("https://example.com/admin/api.php?summaryRaw&auth=validToken")))
.willReturn(request);
given(request.send()).willReturn(response);
given(response.getContentAsString()).willReturn(content);
// When
var result = adminService.summary();
// Then
assertThat(result).contains(dnsStatistics);
}
}

View File

@ -316,6 +316,7 @@
<module>org.openhab.binding.pegelonline</module>
<module>org.openhab.binding.pentair</module>
<module>org.openhab.binding.phc</module>
<module>org.openhab.binding.pihole</module>
<module>org.openhab.binding.pilight</module>
<module>org.openhab.binding.pioneeravr</module>
<module>org.openhab.binding.pixometer</module>