Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 6039d91

Browse files
magx2psmedley
authored andcommittedJul 12, 2024
[pihole] New binding PiHole (openhab#16627)
* Init Pi-hole binding Signed-off-by: Martin Grześlowski <martin.grzeslowski@gmail.com>
1 parent 1e671d5 commit 6039d91

File tree

22 files changed

+1506
-0
lines changed

22 files changed

+1506
-0
lines changed
 

‎CODEOWNERS

+1
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,7 @@
282282
/bundles/org.openhab.binding.pegelonline/ @weymann
283283
/bundles/org.openhab.binding.pentair/ @jsjames
284284
/bundles/org.openhab.binding.phc/ @gnlpfjh
285+
/bundles/org.openhab.binding.pihole/ @magx2
285286
/bundles/org.openhab.binding.pilight/ @stefanroellin @niklasdoerfler
286287
/bundles/org.openhab.binding.pioneeravr/ @Stratehm
287288
/bundles/org.openhab.binding.pixometer/ @Confectrician

‎bom/openhab-addons/pom.xml

+5
Original file line numberDiff line numberDiff line change
@@ -1401,6 +1401,11 @@
14011401
<artifactId>org.openhab.binding.phc</artifactId>
14021402
<version>${project.version}</version>
14031403
</dependency>
1404+
<dependency>
1405+
<groupId>org.openhab.addons.bundles</groupId>
1406+
<artifactId>org.openhab.binding.pihole</artifactId>
1407+
<version>${project.version}</version>
1408+
</dependency>
14041409
<dependency>
14051410
<groupId>org.openhab.addons.bundles</groupId>
14061411
<artifactId>org.openhab.binding.pilight</artifactId>
+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
This content is produced and maintained by the openHAB project.
2+
3+
* Project home: https://www.openhab.org
4+
5+
== Declared Project Licenses
6+
7+
This program and the accompanying materials are made available under the terms
8+
of the Eclipse Public License 2.0 which is available at
9+
https://www.eclipse.org/legal/epl-2.0/.
10+
11+
== Source Code
12+
13+
https://github.com/openhab/openhab-addons
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
# Pi-hole Binding
2+
3+
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.
4+
5+
Pi-hole is a powerful network-level advertisement and internet tracker blocking application.
6+
By intercepting DNS requests, it can prevent unwanted content from being displayed on devices connected to your network.
7+
The Pi-hole Binding allows you to monitor Pi-hole statistics and control its functionality directly from your openHAB setup.
8+
9+
### Features
10+
11+
- Real-time Statistics: Monitor key metrics such as the number of domains being blocked, DNS queries made today, ads blocked today, and more.
12+
- Control: Enable or disable Pi-hole's blocking functionality, configure blocking options, and adjust privacy settings directly from openHAB.
13+
- Integration: Seamlessly integrate Pi-hole data and controls with other openHAB items and rules to create advanced automation scenarios.
14+
15+
## Supported Things
16+
17+
- `server`: Pi-hole server
18+
19+
## Thing Configuration
20+
21+
### `server` Thing Configuration
22+
23+
| Name | Type | Description | Default | Required | Advanced |
24+
|-----------------|---------|-------------------------------------------------------------------------------------------|---------|----------|----------|
25+
| hostname | text | Hostname or IP address of the device | N/A | yes | no |
26+
| token | text | Token to access the device. To generate token go to `settings` > `API` > `Show API token` | N/A | yes | no |
27+
| refreshInterval | integer | Interval the device is polled in sec. | 600 | no | yes |
28+
29+
## Channels
30+
31+
| Channel | Type | Read/Write | Description |
32+
|-------------------------|--------|------------|------------------------------------------------------------|
33+
| domains-being-blocked | Number | RO | The total number of domains currently being blocked. |
34+
| dns-queries-today | Number | RO | The count of DNS queries made today. |
35+
| ads-blocked-today | Number | RO | The number of ads blocked today. |
36+
| ads-percentage-today | Number | RO | The percentage of ads blocked today. |
37+
| unique-domains | Number | RO | The count of unique domains queried. |
38+
| queries-forwarded | Number | RO | The number of queries forwarded to an external DNS server. |
39+
| queries-cached | Number | RO | The number of queries served from the cache. |
40+
| clients-ever-seen | Number | RO | The total number of unique clients ever seen. |
41+
| unique-clients | Number | RO | The current count of unique clients. |
42+
| dns-queries-all-types | Number | RO | The total number of DNS queries of all types. |
43+
| reply-unknown | Number | RO | DNS replies with an unknown status. |
44+
| reply-nodata | Number | RO | DNS replies indicating no data. |
45+
| reply-nxdomain | Number | RO | DNS replies indicating non-existent domain. |
46+
| reply-cname | Number | RO | DNS replies with a CNAME record. |
47+
| reply-ip | Number | RO | DNS replies with an IP address. |
48+
| reply-domain | Number | RO | DNS replies with a domain name. |
49+
| reply-rrname | Number | RO | DNS replies with a resource record name. |
50+
| reply-servfail | Number | RO | DNS replies indicating a server failure. |
51+
| reply-refused | Number | RO | DNS replies indicating refusal. |
52+
| reply-notimp | Number | RO | DNS replies indicating not implemented. |
53+
| reply-other | Number | RO | DNS replies with other statuses. |
54+
| reply-dnssec | Number | RO | DNS replies with DNSSEC information. |
55+
| reply-none | Number | RO | DNS replies with no data. |
56+
| reply-blob | Number | RO | DNS replies with a BLOB (binary large object). |
57+
| dns-queries-all-replies | Number | RO | The total number of DNS queries with all reply types. |
58+
| privacy-level | Number | RO | The privacy level setting. |
59+
| enabled | Switch | RO | The current status of blocking |
60+
| disable-enable | String | RW | Is blocking enabled/disabled |
61+
62+
## Full Example
63+
64+
### Thing Configuration
65+
66+
```java
67+
Thing pihole:server:a4a077edb8 "Pi-hole" @ "Location"
68+
[
69+
refreshIntervalSeconds=600,
70+
hostname="http://123.456.7.89",
71+
token="as654gadf3h1dsfh654dfh6fh7et654asd3g21fh654eth8t4swd4g3s1g65sfg5"
72+
] {
73+
Channels:
74+
Type number : domains_being_blocked "Domains Blocked" [ ]
75+
Type number : dns_queries_today "DNS Queries Today" [ ]
76+
Type number : ads_blocked_today "Ads Blocked Today" [ ]
77+
Type number : ads_percentage_today "Ads Percentage Today" [ ]
78+
Type number : unique_domains "Unique Domains" [ ]
79+
Type number : queries_forwarded "Queries Forwarded" [ ]
80+
Type number : queries_cached "Queries Cached" [ ]
81+
Type number : clients_ever_seen "Clients Ever Seen" [ ]
82+
Type number : unique_clients "Unique Clients" [ ]
83+
Type number : dns_queries_all_types "DNS Queries (All Types)" [ ]
84+
Type number : reply_UNKNOWN "Reply UNKNOWN" [ ]
85+
Type number : reply_NODATA "Reply NODATA" [ ]
86+
Type number : reply_NXDOMAIN "Reply NXDOMAIN" [ ]
87+
Type number : reply_CNAME "Reply CNAME" [ ]
88+
Type number : reply_IP "Reply IP" [ ]
89+
Type number : reply_DOMAIN "Reply DOMAIN" [ ]
90+
Type number : reply_RRNAME "Reply RRNAME" [ ]
91+
Type number : reply_SERVFAIL "Reply SERVFAIL" [ ]
92+
Type number : reply_REFUSED "Reply REFUSED" [ ]
93+
Type number : reply_NOTIMP "Reply NOTIMP" [ ]
94+
Type number : reply_OTHER "Reply OTHER" [ ]
95+
Type number : reply_DNSSEC "Reply DNSSEC" [ ]
96+
Type number : reply_NONE "Reply NONE" [ ]
97+
Type number : reply_BLOB "Reply BLOB" [ ]
98+
Type number : dns_queries_all_replies "DNS Queries (All Replies)" [ ]
99+
Type number : privacy_level "Privacy Level" [ ]
100+
Type switch : enabled "Status" [ ]
101+
Type string : disable-enable "Disable Blocking" [ ]
102+
}
103+
```
104+
105+
### Item Configuration
106+
107+
```java
108+
Number domains_being_blocked "Domains Blocked" { channel="pihole:server:a4a077edb8:domains_being_blocked" }
109+
Number dns_queries_today "DNS Queries Today" { channel="pihole:server:a4a077edb8:dns_queries_today" }
110+
Number ads_blocked_today "Ads Blocked Today" { channel="pihole:server:a4a077edb8:ads_blocked_today" }
111+
Number ads_percentage_today "Ads Percentage Today" { channel="pihole:server:a4a077edb8:ads_percentage_today" }
112+
Number unique_domains "Unique Domains" { channel="pihole:server:a4a077edb8:unique_domains" }
113+
Number queries_forwarded "Queries Forwarded" { channel="pihole:server:a4a077edb8:queries_forwarded" }
114+
Number queries_cached "Queries Cached" { channel="pihole:server:a4a077edb8:queries_cached" }
115+
Number clients_ever_seen "Clients Ever Seen" { channel="pihole:server:a4a077edb8:clients_ever_seen" }
116+
Number unique_clients "Unique Clients" { channel="pihole:server:a4a077edb8:unique_clients" }
117+
Number dns_queries_all_types "DNS Queries (All Types)" { channel="pihole:server:a4a077edb8:dns_queries_all_types" }
118+
Number reply_UNKNOWN "Reply UNKNOWN" { channel="pihole:server:a4a077edb8:reply_UNKNOWN" }
119+
Number reply_NODATA "Reply NODATA" { channel="pihole:server:a4a077edb8:reply_NODATA" }
120+
Number reply_NXDOMAIN "Reply NXDOMAIN" { channel="pihole:server:a4a077edb8:reply_NXDOMAIN" }
121+
Number reply_CNAME "Reply CNAME" { channel="pihole:server:a4a077edb8:reply_CNAME" }
122+
Number reply_IP "Reply IP" { channel="pihole:server:a4a077edb8:reply_IP" }
123+
Number reply_DOMAIN "Reply DOMAIN" { channel="pihole:server:a4a077edb8:reply_DOMAIN" }
124+
Number reply_RRNAME "Reply RRNAME" { channel="pihole:server:a4a077edb8:reply_RRNAME" }
125+
Number reply_SERVFAIL "Reply SERVFAIL" { channel="pihole:server:a4a077edb8:reply_SERVFAIL" }
126+
Number reply_REFUSED "Reply REFUSED" { channel="pihole:server:a4a077edb8:reply_REFUSED" }
127+
Number reply_NOTIMP "Reply NOTIMP" { channel="pihole:server:a4a077edb8:reply_NOTIMP" }
128+
Number reply_OTHER "Reply OTHER" { channel="pihole:server:a4a077edb8:reply_OTHER" }
129+
Number reply_DNSSEC "Reply DNSSEC" { channel="pihole:server:a4a077edb8:reply_DNSSEC" }
130+
Number reply_NONE "Reply NONE" { channel="pihole:server:a4a077edb8:reply_NONE" }
131+
Number reply_BLOB "Reply BLOB" { channel="pihole:server:a4a077edb8:reply_BLOB" }
132+
Number dns_queries_all_replies "DNS Queries (All Replies)" { channel="pihole:server:a4a077edb8:dns_queries_all_replies" }
133+
Number privacy_level "Privacy Level" { channel="pihole:server:a4a077edb8:privacy_level" }
134+
Switch enabled "Status" { channel="pihole:server:a4a077edb8:enabled" }
135+
String disable_enable "Disable Blocking" { channel="pihole:server:a4a077edb8:disable-enable" }
136+
```
137+
138+
### Actions
139+
140+
Pi-hole binding provides actions to use in rules:
141+
142+
```java
143+
import java.util.concurrent.TimeUnit
144+
145+
rule "test"
146+
when
147+
/* when */
148+
then
149+
val actions = getActions("pihole", "pihole:server:as8af03m38")
150+
if (actions !== null) {
151+
// disable blocking for 5 * 60 seconds (5 minutes)
152+
actions.disableBlocking(5 * 60)
153+
154+
// disable blocking for 5 minutes
155+
actions.disableBlocking(5, TimeUnit.MINUTES)
156+
157+
// disable blocking for infinity
158+
actions.disableBlocking(0)
159+
actions.disableBlocking()
160+
161+
// enable blocking
162+
actions.enableBlocking()
163+
}
164+
end
165+
```
+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
3+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
4+
5+
<modelVersion>4.0.0</modelVersion>
6+
7+
<parent>
8+
<groupId>org.openhab.addons.bundles</groupId>
9+
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
10+
<version>4.3.0-SNAPSHOT</version>
11+
</parent>
12+
13+
<artifactId>org.openhab.binding.pihole</artifactId>
14+
15+
<name>openHAB Add-ons :: Bundles :: Pi-hole Binding</name>
16+
17+
<dependencies>
18+
<dependency>
19+
<groupId>org.assertj</groupId>
20+
<artifactId>assertj-core</artifactId>
21+
<version>3.25.3</version>
22+
<scope>test</scope>
23+
</dependency>
24+
<dependency>
25+
<groupId>org.mockito</groupId>
26+
<artifactId>mockito-core</artifactId>
27+
<version>5.11.0</version>
28+
<scope>test</scope>
29+
</dependency>
30+
</dependencies>
31+
</project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<features name="org.openhab.binding.pihole-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
3+
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
4+
5+
<feature name="openhab-binding-pihole" description="Pi-hole Binding" version="${project.version}">
6+
<feature>openhab-runtime-base</feature>
7+
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.pihole/${project.version}</bundle>
8+
</feature>
9+
</features>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/**
2+
* Copyright (c) 2010-2024 Contributors to the openHAB project
3+
*
4+
* See the NOTICE file(s) distributed with this work for additional
5+
* information.
6+
*
7+
* This program and the accompanying materials are made available under the
8+
* terms of the Eclipse Public License 2.0 which is available at
9+
* http://www.eclipse.org/legal/epl-2.0
10+
*
11+
* SPDX-License-Identifier: EPL-2.0
12+
*/
13+
package org.openhab.binding.pihole.internal;
14+
15+
import static java.util.Objects.requireNonNull;
16+
import static java.util.concurrent.TimeUnit.SECONDS;
17+
import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.BINDING_ID;
18+
19+
import java.util.concurrent.TimeUnit;
20+
21+
import org.eclipse.jdt.annotation.NonNullByDefault;
22+
import org.eclipse.jdt.annotation.Nullable;
23+
import org.openhab.core.automation.annotation.ActionInput;
24+
import org.openhab.core.automation.annotation.RuleAction;
25+
import org.openhab.core.thing.binding.ThingActions;
26+
import org.openhab.core.thing.binding.ThingActionsScope;
27+
import org.openhab.core.thing.binding.ThingHandler;
28+
29+
/**
30+
* @author Martin Grzeslowski - Initial contribution
31+
*/
32+
@ThingActionsScope(name = BINDING_ID)
33+
@NonNullByDefault
34+
public class PiHoleActions implements ThingActions {
35+
private @Nullable PiHoleHandler handler;
36+
37+
@Override
38+
public void setThingHandler(@Nullable ThingHandler handler) {
39+
this.handler = (PiHoleHandler) handler;
40+
}
41+
42+
@Override
43+
public @Nullable ThingHandler getThingHandler() {
44+
return handler;
45+
}
46+
47+
@RuleAction(label = "@text/action.disable.label", description = "@text/action.disable.description")
48+
public void disableBlocking(
49+
@ActionInput(name = "time", label = "@text/action.disable.timeLabel", description = "@text/action.disable.timeDescription") long time,
50+
@ActionInput(name = "timeUnit", label = "@text/action.disable.timeUnitLabel", description = "@text/action.disable.timeUnitDescription") @Nullable TimeUnit timeUnit)
51+
throws PiHoleException {
52+
if (time < 0) {
53+
return;
54+
}
55+
56+
if (timeUnit == null) {
57+
timeUnit = SECONDS;
58+
}
59+
60+
var local = handler;
61+
if (local == null) {
62+
return;
63+
}
64+
local.disableBlocking(timeUnit.toSeconds(time));
65+
}
66+
67+
public static void disableBlocking(@Nullable ThingActions actions, long time, @Nullable TimeUnit timeUnit)
68+
throws PiHoleException {
69+
((PiHoleActions) requireNonNull(actions)).disableBlocking(time, timeUnit);
70+
}
71+
72+
@RuleAction(label = "@text/action.disable.label", description = "@text/action.disable.description")
73+
public void disableBlocking(
74+
@ActionInput(name = "time", label = "@text/action.disable.timeLabel", description = "@text/action.disable.timeDescription") long time)
75+
throws PiHoleException {
76+
disableBlocking(time, null);
77+
}
78+
79+
public static void disableBlocking(@Nullable ThingActions actions, long time) throws PiHoleException {
80+
((PiHoleActions) requireNonNull(actions)).disableBlocking(time);
81+
}
82+
83+
@RuleAction(label = "@text/action.disableInf.label", description = "@text/action.disableInf.description")
84+
public void disableBlocking() throws PiHoleException {
85+
disableBlocking(0, null);
86+
}
87+
88+
public static void disableBlocking(@Nullable ThingActions actions) throws PiHoleException {
89+
((PiHoleActions) requireNonNull(actions)).disableBlocking(0);
90+
}
91+
92+
@RuleAction(label = "@text/action.enable.label", description = "@text/action.enable.description")
93+
public void enableBlocking() throws PiHoleException {
94+
var local = handler;
95+
if (local == null) {
96+
return;
97+
}
98+
local.enableBlocking();
99+
}
100+
101+
public static void enableBlocking(@Nullable ThingActions actions) throws PiHoleException {
102+
((PiHoleActions) requireNonNull(actions)).enableBlocking();
103+
}
104+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/**
2+
* Copyright (c) 2010-2024 Contributors to the openHAB project
3+
*
4+
* See the NOTICE file(s) distributed with this work for additional
5+
* information.
6+
*
7+
* This program and the accompanying materials are made available under the
8+
* terms of the Eclipse Public License 2.0 which is available at
9+
* http://www.eclipse.org/legal/epl-2.0
10+
*
11+
* SPDX-License-Identifier: EPL-2.0
12+
*/
13+
package org.openhab.binding.pihole.internal;
14+
15+
import org.eclipse.jdt.annotation.NonNullByDefault;
16+
import org.openhab.core.thing.ThingTypeUID;
17+
18+
/**
19+
* The {@link PiHoleBindingConstants} class defines common constants, which are
20+
* used across the whole binding.
21+
*
22+
* @author Martin Grzeslowski - Initial contribution
23+
*/
24+
@NonNullByDefault
25+
public class PiHoleBindingConstants {
26+
27+
public static final String BINDING_ID = "pihole";
28+
29+
// List of all Thing Type UIDs
30+
public static final ThingTypeUID PI_HOLE_TYPE = new ThingTypeUID(BINDING_ID, "server");
31+
32+
public static final class Channels {
33+
public static final String DOMAINS_BEING_BLOCKED_CHANNEL = "domains-being-blocked";
34+
public static final String DNS_QUERIES_TODAY_CHANNEL = "dns-queries-today";
35+
public static final String ADS_BLOCKED_TODAY_CHANNEL = "ads-blocked-today";
36+
public static final String ADS_PERCENTAGE_TODAY_CHANNEL = "ads-percentage-today";
37+
public static final String UNIQUE_DOMAINS_CHANNEL = "unique-domains";
38+
public static final String QUERIES_FORWARDED_CHANNEL = "queries-forwarded";
39+
public static final String QUERIES_CACHED_CHANNEL = "queries-cached";
40+
public static final String CLIENTS_EVER_SEEN_CHANNEL = "clients-ever-seen";
41+
public static final String UNIQUE_CLIENTS_CHANNEL = "unique-clients";
42+
public static final String DNS_QUERIES_ALL_TYPES_CHANNEL = "dns-queries-all-types";
43+
public static final String REPLY_UNKNOWN_CHANNEL = "reply-unknown";
44+
public static final String REPLY_NODATA_CHANNEL = "reply-nodata";
45+
public static final String REPLY_NXDOMAIN_CHANNEL = "reply-nxdomain";
46+
public static final String REPLY_CNAME_CHANNEL = "reply-cname";
47+
public static final String REPLY_IP_CHANNEL = "reply-ip";
48+
public static final String REPLY_DOMAIN_CHANNEL = "reply-domain";
49+
public static final String REPLY_RRNAME_CHANNEL = "reply-rrname";
50+
public static final String REPLY_SERVFAIL_CHANNEL = "reply-servfail";
51+
public static final String REPLY_REFUSED_CHANNEL = "reply-refused";
52+
public static final String REPLY_NOTIMP_CHANNEL = "reply-notimp";
53+
public static final String REPLY_OTHER_CHANNEL = "reply-other";
54+
public static final String REPLY_DNSSEC_CHANNEL = "reply-dnssec";
55+
public static final String REPLY_NONE_CHANNEL = "reply-none";
56+
public static final String REPLY_BLOB_CHANNEL = "reply-blob";
57+
public static final String DNS_QUERIES_ALL_REPLIES_CHANNEL = "dns-queries-all-replies";
58+
public static final String PRIVACY_LEVEL_CHANNEL = "privacy-level";
59+
public static final String ENABLED_CHANNEL = "enabled";
60+
public static final String DISABLE_ENABLE_CHANNEL = "disable-enable";
61+
62+
public static enum DisableEnable {
63+
DISABLE,
64+
FOR_10_SEC,
65+
FOR_30_SEC,
66+
FOR_5_MIN,
67+
ENABLE
68+
}
69+
}
70+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/**
2+
* Copyright (c) 2010-2024 Contributors to the openHAB project
3+
*
4+
* See the NOTICE file(s) distributed with this work for additional
5+
* information.
6+
*
7+
* This program and the accompanying materials are made available under the
8+
* terms of the Eclipse Public License 2.0 which is available at
9+
* http://www.eclipse.org/legal/epl-2.0
10+
*
11+
* SPDX-License-Identifier: EPL-2.0
12+
*/
13+
package org.openhab.binding.pihole.internal;
14+
15+
import org.eclipse.jdt.annotation.NonNullByDefault;
16+
17+
/**
18+
* The {@link PiHoleConfiguration} class contains fields mapping thing configuration parameters.
19+
*
20+
* @author Martin Grzeslowski - Initial contribution
21+
*/
22+
@NonNullByDefault
23+
public class PiHoleConfiguration {
24+
public String hostname = "";
25+
public String token = "";
26+
public int refreshIntervalSeconds = 600;
27+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/**
2+
* Copyright (c) 2010-2024 Contributors to the openHAB project
3+
*
4+
* See the NOTICE file(s) distributed with this work for additional
5+
* information.
6+
*
7+
* This program and the accompanying materials are made available under the
8+
* terms of the Eclipse Public License 2.0 which is available at
9+
* http://www.eclipse.org/legal/epl-2.0
10+
*
11+
* SPDX-License-Identifier: EPL-2.0
12+
*/
13+
package org.openhab.binding.pihole.internal;
14+
15+
import java.io.Serial;
16+
17+
import org.eclipse.jdt.annotation.NonNullByDefault;
18+
19+
/**
20+
* @author Martin Grzeslowski - Initial contribution
21+
*/
22+
@NonNullByDefault
23+
public class PiHoleException extends Exception {
24+
@Serial
25+
private static final long serialVersionUID = 1L;
26+
27+
public PiHoleException(String message) {
28+
super(message);
29+
}
30+
31+
public PiHoleException(String message, Throwable cause) {
32+
super(message, cause);
33+
}
34+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
/**
2+
* Copyright (c) 2010-2024 Contributors to the openHAB project
3+
*
4+
* See the NOTICE file(s) distributed with this work for additional
5+
* information.
6+
*
7+
* This program and the accompanying materials are made available under the
8+
* terms of the Eclipse Public License 2.0 which is available at
9+
* http://www.eclipse.org/legal/epl-2.0
10+
*
11+
* SPDX-License-Identifier: EPL-2.0
12+
*/
13+
package org.openhab.binding.pihole.internal;
14+
15+
import static java.util.concurrent.TimeUnit.*;
16+
import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.ADS_BLOCKED_TODAY_CHANNEL;
17+
import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.ADS_PERCENTAGE_TODAY_CHANNEL;
18+
import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.CLIENTS_EVER_SEEN_CHANNEL;
19+
import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.DISABLE_ENABLE_CHANNEL;
20+
import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.DNS_QUERIES_ALL_REPLIES_CHANNEL;
21+
import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.DNS_QUERIES_ALL_TYPES_CHANNEL;
22+
import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.DNS_QUERIES_TODAY_CHANNEL;
23+
import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.DOMAINS_BEING_BLOCKED_CHANNEL;
24+
import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.DisableEnable;
25+
import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.DisableEnable.ENABLE;
26+
import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.ENABLED_CHANNEL;
27+
import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.PRIVACY_LEVEL_CHANNEL;
28+
import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.QUERIES_CACHED_CHANNEL;
29+
import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.QUERIES_FORWARDED_CHANNEL;
30+
import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.REPLY_BLOB_CHANNEL;
31+
import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.REPLY_CNAME_CHANNEL;
32+
import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.REPLY_DNSSEC_CHANNEL;
33+
import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.REPLY_DOMAIN_CHANNEL;
34+
import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.REPLY_IP_CHANNEL;
35+
import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.REPLY_NODATA_CHANNEL;
36+
import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.REPLY_NONE_CHANNEL;
37+
import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.REPLY_NOTIMP_CHANNEL;
38+
import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.REPLY_NXDOMAIN_CHANNEL;
39+
import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.REPLY_OTHER_CHANNEL;
40+
import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.REPLY_REFUSED_CHANNEL;
41+
import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.REPLY_RRNAME_CHANNEL;
42+
import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.REPLY_SERVFAIL_CHANNEL;
43+
import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.REPLY_UNKNOWN_CHANNEL;
44+
import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.UNIQUE_CLIENTS_CHANNEL;
45+
import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.UNIQUE_DOMAINS_CHANNEL;
46+
import static org.openhab.core.library.unit.Units.PERCENT;
47+
import static org.openhab.core.thing.ThingStatus.OFFLINE;
48+
import static org.openhab.core.thing.ThingStatus.ONLINE;
49+
import static org.openhab.core.thing.ThingStatus.UNKNOWN;
50+
import static org.openhab.core.thing.ThingStatusDetail.*;
51+
52+
import java.math.BigDecimal;
53+
import java.net.URI;
54+
import java.net.URISyntaxException;
55+
import java.util.Collection;
56+
import java.util.Optional;
57+
import java.util.Set;
58+
import java.util.concurrent.ScheduledFuture;
59+
60+
import org.eclipse.jdt.annotation.NonNullByDefault;
61+
import org.eclipse.jdt.annotation.Nullable;
62+
import org.eclipse.jetty.client.HttpClient;
63+
import org.openhab.binding.pihole.internal.rest.AdminService;
64+
import org.openhab.binding.pihole.internal.rest.JettyAdminService;
65+
import org.openhab.binding.pihole.internal.rest.model.DnsStatistics;
66+
import org.openhab.core.library.types.DecimalType;
67+
import org.openhab.core.library.types.OnOffType;
68+
import org.openhab.core.library.types.QuantityType;
69+
import org.openhab.core.library.types.StringType;
70+
import org.openhab.core.thing.ChannelUID;
71+
import org.openhab.core.thing.Thing;
72+
import org.openhab.core.thing.binding.BaseThingHandler;
73+
import org.openhab.core.thing.binding.ThingHandlerService;
74+
import org.openhab.core.types.Command;
75+
import org.openhab.core.types.RefreshType;
76+
import org.slf4j.Logger;
77+
import org.slf4j.LoggerFactory;
78+
79+
/**
80+
* The {@link PiHoleHandler} is responsible for handling commands, which are
81+
* sent to one of the channels.
82+
*
83+
* @author Martin Grzeslowski - Initial contribution
84+
*/
85+
@NonNullByDefault
86+
public class PiHoleHandler extends BaseThingHandler implements AdminService {
87+
private static final int HTTP_DELAY_SECONDS = 1;
88+
private final Logger logger = LoggerFactory.getLogger(PiHoleHandler.class);
89+
private final Object lock = new Object();
90+
private final HttpClient httpClient;
91+
92+
private @Nullable AdminService adminService;
93+
private @Nullable DnsStatistics dnsStatistics;
94+
private @Nullable ScheduledFuture<?> scheduledFuture;
95+
96+
public PiHoleHandler(Thing thing, HttpClient httpClient) {
97+
super(thing);
98+
this.httpClient = httpClient;
99+
}
100+
101+
@Override
102+
public void initialize() {
103+
// set the thing status to UNKNOWN temporarily and let the background task decide for the real status.
104+
// the framework is then able to reuse the resources from the thing handler initialization.
105+
// we set this upfront to reliably check status updates in unit tests.
106+
updateStatus(UNKNOWN);
107+
108+
var config = getConfigAs(PiHoleConfiguration.class);
109+
110+
if (config.refreshIntervalSeconds <= 0) {
111+
updateStatus(OFFLINE, CONFIGURATION_ERROR, "@text/handler.init.wrongInterval");
112+
return;
113+
}
114+
115+
URI hostname;
116+
try {
117+
hostname = new URI(config.hostname);
118+
} catch (URISyntaxException e) {
119+
updateStatus(OFFLINE, CONFIGURATION_ERROR,
120+
"@token/handler.init.invalidHostname[\"" + config.hostname + "\"]");
121+
return;
122+
}
123+
if (config.token.isEmpty()) {
124+
updateStatus(OFFLINE, CONFIGURATION_ERROR, "@token/handler.init.noToken");
125+
return;
126+
}
127+
adminService = new JettyAdminService(config.token, hostname, httpClient);
128+
scheduledFuture = scheduler.scheduleWithFixedDelay(this::update, 0, config.refreshIntervalSeconds, SECONDS);
129+
130+
// do not set status here, the background task will do it.
131+
}
132+
133+
private void update() {
134+
var local = adminService;
135+
if (local == null) {
136+
return;
137+
}
138+
139+
// this block can be called from at least 2 threads
140+
// check disableBlocking method
141+
synchronized (lock) {
142+
try {
143+
logger.debug("Refreshing DnsStatistics from Pi-hole");
144+
local.summary().ifPresent(statistics -> dnsStatistics = statistics);
145+
refresh();
146+
updateStatus(ONLINE);
147+
} catch (Exception e) {
148+
logger.debug("Error occurred when refreshing DnsStatistics from Pi-hole", e);
149+
updateStatus(OFFLINE, COMMUNICATION_ERROR, e.getLocalizedMessage());
150+
}
151+
}
152+
}
153+
154+
@Override
155+
public void handleCommand(ChannelUID channelUID, Command command) {
156+
if (command instanceof RefreshType) {
157+
refresh();
158+
return;
159+
}
160+
161+
if (DISABLE_ENABLE_CHANNEL.equals(channelUID.getId())) {
162+
if (command instanceof StringType stringType) {
163+
var value = DisableEnable.valueOf(stringType.toString());
164+
try {
165+
switch (value) {
166+
case DISABLE -> disableBlocking(0);
167+
case FOR_10_SEC -> disableBlocking(10);
168+
case FOR_30_SEC -> disableBlocking(30);
169+
case FOR_5_MIN -> disableBlocking(MINUTES.toSeconds(5));
170+
case ENABLE -> enableBlocking();
171+
}
172+
} catch (PiHoleException ex) {
173+
logger.debug("Cannot invoke {} on channel {}", value, channelUID, ex);
174+
updateStatus(OFFLINE, COMMUNICATION_ERROR, ex.getLocalizedMessage());
175+
}
176+
}
177+
}
178+
}
179+
180+
private void refresh() {
181+
var localDnsStatistics = dnsStatistics;
182+
if (localDnsStatistics == null) {
183+
return;
184+
}
185+
186+
updateDecimalState(DOMAINS_BEING_BLOCKED_CHANNEL, localDnsStatistics.domainsBeingBlocked());
187+
updateDecimalState(DNS_QUERIES_TODAY_CHANNEL, localDnsStatistics.dnsQueriesToday());
188+
updateDecimalState(ADS_BLOCKED_TODAY_CHANNEL, localDnsStatistics.adsBlockedToday());
189+
updateDecimalState(UNIQUE_DOMAINS_CHANNEL, localDnsStatistics.uniqueDomains());
190+
updateDecimalState(QUERIES_FORWARDED_CHANNEL, localDnsStatistics.queriesForwarded());
191+
updateDecimalState(QUERIES_CACHED_CHANNEL, localDnsStatistics.queriesCached());
192+
updateDecimalState(CLIENTS_EVER_SEEN_CHANNEL, localDnsStatistics.clientsEverSeen());
193+
updateDecimalState(UNIQUE_CLIENTS_CHANNEL, localDnsStatistics.uniqueClients());
194+
updateDecimalState(DNS_QUERIES_ALL_TYPES_CHANNEL, localDnsStatistics.dnsQueriesAllTypes());
195+
updateDecimalState(REPLY_UNKNOWN_CHANNEL, localDnsStatistics.replyUnknown());
196+
updateDecimalState(REPLY_NODATA_CHANNEL, localDnsStatistics.replyNoData());
197+
updateDecimalState(REPLY_NXDOMAIN_CHANNEL, localDnsStatistics.replyNXDomain());
198+
updateDecimalState(REPLY_CNAME_CHANNEL, localDnsStatistics.replyCName());
199+
updateDecimalState(REPLY_IP_CHANNEL, localDnsStatistics.replyIP());
200+
updateDecimalState(REPLY_DOMAIN_CHANNEL, localDnsStatistics.replyDomain());
201+
updateDecimalState(REPLY_RRNAME_CHANNEL, localDnsStatistics.replyRRName());
202+
updateDecimalState(REPLY_SERVFAIL_CHANNEL, localDnsStatistics.replyServFail());
203+
updateDecimalState(REPLY_REFUSED_CHANNEL, localDnsStatistics.replyRefused());
204+
updateDecimalState(REPLY_NOTIMP_CHANNEL, localDnsStatistics.replyNotImp());
205+
updateDecimalState(REPLY_OTHER_CHANNEL, localDnsStatistics.replyOther());
206+
updateDecimalState(REPLY_DNSSEC_CHANNEL, localDnsStatistics.replyDNSSEC());
207+
updateDecimalState(REPLY_NONE_CHANNEL, localDnsStatistics.replyNone());
208+
updateDecimalState(REPLY_BLOB_CHANNEL, localDnsStatistics.replyBlob());
209+
updateDecimalState(DNS_QUERIES_ALL_REPLIES_CHANNEL, localDnsStatistics.dnsQueriesAllTypes());
210+
updateDecimalState(PRIVACY_LEVEL_CHANNEL, localDnsStatistics.privacyLevel());
211+
212+
var adsPercentageToday = localDnsStatistics.adsPercentageToday();
213+
if (adsPercentageToday != null) {
214+
var state = new QuantityType<>(new BigDecimal(adsPercentageToday.toString()), PERCENT);
215+
updateState(ADS_PERCENTAGE_TODAY_CHANNEL, state);
216+
}
217+
updateState(ENABLED_CHANNEL, OnOffType.from(localDnsStatistics.enabled()));
218+
if (localDnsStatistics.enabled()) {
219+
updateState(DISABLE_ENABLE_CHANNEL, new StringType(ENABLE.toString()));
220+
}
221+
}
222+
223+
private void updateDecimalState(String channelID, @Nullable Integer value) {
224+
if (value == null) {
225+
return;
226+
}
227+
updateState(channelID, new DecimalType(value));
228+
}
229+
230+
@Override
231+
public Collection<Class<? extends ThingHandlerService>> getServices() {
232+
return Set.of(PiHoleActions.class);
233+
}
234+
235+
@Override
236+
public void dispose() {
237+
adminService = null;
238+
dnsStatistics = null;
239+
var localScheduledFuture = scheduledFuture;
240+
if (localScheduledFuture != null) {
241+
localScheduledFuture.cancel(true);
242+
scheduledFuture = null;
243+
}
244+
super.dispose();
245+
}
246+
247+
@Override
248+
public Optional<DnsStatistics> summary() throws PiHoleException {
249+
var local = adminService;
250+
if (local == null) {
251+
throw new IllegalStateException("AdminService not initialized");
252+
}
253+
return local.summary();
254+
}
255+
256+
@Override
257+
public void disableBlocking(long seconds) throws PiHoleException {
258+
var local = adminService;
259+
if (local == null) {
260+
throw new IllegalStateException("AdminService not initialized");
261+
}
262+
local.disableBlocking(seconds);
263+
// update the summary to get the value of DISABLED_CHANNEL channel
264+
scheduler.schedule(this::update, HTTP_DELAY_SECONDS, SECONDS);
265+
if (seconds > 0) {
266+
// update the summary to get the value of ENABLED_CHANNEL channel
267+
// after the X seconds it probably will be true again
268+
scheduler.schedule(this::update, seconds + HTTP_DELAY_SECONDS, SECONDS);
269+
}
270+
}
271+
272+
@Override
273+
public void enableBlocking() throws PiHoleException {
274+
var local = adminService;
275+
if (local == null) {
276+
throw new IllegalStateException("AdminService not initialized");
277+
}
278+
local.enableBlocking();
279+
// update the summary to get the value of DISABLED_CHANNEL channel
280+
scheduler.schedule(this::update, HTTP_DELAY_SECONDS, SECONDS);
281+
}
282+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/**
2+
* Copyright (c) 2010-2024 Contributors to the openHAB project
3+
*
4+
* See the NOTICE file(s) distributed with this work for additional
5+
* information.
6+
*
7+
* This program and the accompanying materials are made available under the
8+
* terms of the Eclipse Public License 2.0 which is available at
9+
* http://www.eclipse.org/legal/epl-2.0
10+
*
11+
* SPDX-License-Identifier: EPL-2.0
12+
*/
13+
package org.openhab.binding.pihole.internal;
14+
15+
import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.PI_HOLE_TYPE;
16+
17+
import java.util.Set;
18+
19+
import org.eclipse.jdt.annotation.NonNullByDefault;
20+
import org.eclipse.jdt.annotation.Nullable;
21+
import org.openhab.core.io.net.http.HttpClientFactory;
22+
import org.openhab.core.thing.Thing;
23+
import org.openhab.core.thing.ThingTypeUID;
24+
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
25+
import org.openhab.core.thing.binding.ThingHandler;
26+
import org.openhab.core.thing.binding.ThingHandlerFactory;
27+
import org.osgi.service.component.annotations.Activate;
28+
import org.osgi.service.component.annotations.Component;
29+
import org.osgi.service.component.annotations.Reference;
30+
31+
/**
32+
* The {@link PiHoleHandlerFactory} is responsible for creating things and thing
33+
* handlers.
34+
*
35+
* @author Martin Grzeslowski - Initial contribution
36+
*/
37+
@NonNullByDefault
38+
@Component(configurationPid = "binding.pihole", service = ThingHandlerFactory.class)
39+
public class PiHoleHandlerFactory extends BaseThingHandlerFactory {
40+
41+
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(PI_HOLE_TYPE);
42+
private final HttpClientFactory httpClientFactory;
43+
44+
@Activate
45+
public PiHoleHandlerFactory(@Reference HttpClientFactory httpClientFactory) {
46+
this.httpClientFactory = httpClientFactory;
47+
}
48+
49+
@Override
50+
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
51+
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
52+
}
53+
54+
@Override
55+
protected @Nullable ThingHandler createHandler(Thing thing) {
56+
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
57+
58+
if (PI_HOLE_TYPE.equals(thingTypeUID)) {
59+
return new PiHoleHandler(thing, httpClientFactory.getCommonHttpClient());
60+
}
61+
62+
return null;
63+
}
64+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/**
2+
* Copyright (c) 2010-2024 Contributors to the openHAB project
3+
*
4+
* See the NOTICE file(s) distributed with this work for additional
5+
* information.
6+
*
7+
* This program and the accompanying materials are made available under the
8+
* terms of the Eclipse Public License 2.0 which is available at
9+
* http://www.eclipse.org/legal/epl-2.0
10+
*
11+
* SPDX-License-Identifier: EPL-2.0
12+
*/
13+
package org.openhab.binding.pihole.internal.rest;
14+
15+
import java.util.Optional;
16+
17+
import org.eclipse.jdt.annotation.NonNullByDefault;
18+
import org.openhab.binding.pihole.internal.PiHoleException;
19+
import org.openhab.binding.pihole.internal.rest.model.DnsStatistics;
20+
21+
/**
22+
* @author Martin Grzeslowski - Initial contribution
23+
*/
24+
@NonNullByDefault
25+
public interface AdminService {
26+
/**
27+
* Retrieves a summary of DNS statistics.
28+
*
29+
* @return An optional containing the DNS statistics.
30+
* @throws PiHoleException In case of error
31+
*/
32+
Optional<DnsStatistics> summary() throws PiHoleException;
33+
34+
/**
35+
* Disables blocking for a specified duration.
36+
*
37+
* @param seconds The duration in seconds for which blocking should be disabled.
38+
* @throws PiHoleException In case of error
39+
*/
40+
void disableBlocking(long seconds) throws PiHoleException;
41+
42+
/**
43+
* Enables blocking.
44+
*
45+
* @throws PiHoleException In case of error
46+
*/
47+
void enableBlocking() throws PiHoleException;
48+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/**
2+
* Copyright (c) 2010-2024 Contributors to the openHAB project
3+
*
4+
* See the NOTICE file(s) distributed with this work for additional
5+
* information.
6+
*
7+
* This program and the accompanying materials are made available under the
8+
* terms of the Eclipse Public License 2.0 which is available at
9+
* http://www.eclipse.org/legal/epl-2.0
10+
*
11+
* SPDX-License-Identifier: EPL-2.0
12+
*/
13+
package org.openhab.binding.pihole.internal.rest;
14+
15+
import static java.util.concurrent.TimeUnit.SECONDS;
16+
17+
import java.net.URI;
18+
import java.util.Optional;
19+
import java.util.concurrent.ExecutionException;
20+
import java.util.concurrent.TimeoutException;
21+
22+
import org.eclipse.jdt.annotation.NonNullByDefault;
23+
import org.eclipse.jetty.client.HttpClient;
24+
import org.eclipse.jetty.client.api.ContentResponse;
25+
import org.eclipse.jetty.client.api.Request;
26+
import org.openhab.binding.pihole.internal.PiHoleException;
27+
import org.openhab.binding.pihole.internal.rest.model.DnsStatistics;
28+
import org.slf4j.Logger;
29+
import org.slf4j.LoggerFactory;
30+
31+
import com.google.gson.FieldNamingPolicy;
32+
import com.google.gson.Gson;
33+
import com.google.gson.GsonBuilder;
34+
35+
/**
36+
* @author Martin Grzeslowski - Initial contribution
37+
*/
38+
@NonNullByDefault
39+
public class JettyAdminService implements AdminService {
40+
private static final Gson GSON = new GsonBuilder()
41+
.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create();
42+
private static final long TIMEOUT_SECONDS = 10L;
43+
private final Logger logger = LoggerFactory.getLogger(JettyAdminService.class);
44+
private final String token;
45+
private final URI baseUrl;
46+
private final HttpClient client;
47+
48+
public JettyAdminService(String token, URI baseUrl, HttpClient client) {
49+
this.token = token;
50+
this.baseUrl = baseUrl;
51+
this.client = client;
52+
}
53+
54+
@Override
55+
public Optional<DnsStatistics> summary() throws PiHoleException {
56+
logger.debug("Getting summary");
57+
var url = baseUrl.resolve("/admin/api.php?summaryRaw&auth=" + token);
58+
var request = client.newRequest(url).timeout(TIMEOUT_SECONDS, SECONDS);
59+
var response = send(request);
60+
var content = response.getContentAsString();
61+
return Optional.ofNullable(GSON.fromJson(content, DnsStatistics.class));
62+
}
63+
64+
private static ContentResponse send(Request request) throws PiHoleException {
65+
try {
66+
return request.send();
67+
} catch (InterruptedException | TimeoutException | ExecutionException e) {
68+
throw new PiHoleException(
69+
"Exception while sending request to Pi-hole. %s".formatted(e.getLocalizedMessage()), e);
70+
}
71+
}
72+
73+
@Override
74+
public void disableBlocking(long seconds) throws PiHoleException {
75+
logger.debug("Disabling blocking for {} seconds", seconds);
76+
var url = baseUrl.resolve("/admin/api.php?disable=%s&auth=%s".formatted(seconds, token));
77+
var request = client.newRequest(url).timeout(TIMEOUT_SECONDS, SECONDS);
78+
send(request);
79+
}
80+
81+
@Override
82+
public void enableBlocking() throws PiHoleException {
83+
logger.debug("Enabling blocking");
84+
var url = baseUrl.resolve("/admin/api.php?disable&auth=%s".formatted(token));
85+
var request = client.newRequest(url).timeout(TIMEOUT_SECONDS, SECONDS);
86+
send(request);
87+
}
88+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/**
2+
* Copyright (c) 2010-2024 Contributors to the openHAB project
3+
*
4+
* See the NOTICE file(s) distributed with this work for additional
5+
* information.
6+
*
7+
* This program and the accompanying materials are made available under the
8+
* terms of the Eclipse Public License 2.0 which is available at
9+
* http://www.eclipse.org/legal/epl-2.0
10+
*
11+
* SPDX-License-Identifier: EPL-2.0
12+
*/
13+
package org.openhab.binding.pihole.internal.rest.model;
14+
15+
import org.eclipse.jdt.annotation.NonNullByDefault;
16+
import org.eclipse.jdt.annotation.Nullable;
17+
18+
import com.google.gson.annotations.SerializedName;
19+
20+
/**
21+
* @author Martin Grzeslowski - Initial contribution
22+
*/
23+
@NonNullByDefault
24+
public record DnsStatistics(@Nullable Integer domainsBeingBlocked, @Nullable Integer dnsQueriesToday,
25+
@Nullable Integer adsBlockedToday, @Nullable Double adsPercentageToday, @Nullable Integer uniqueDomains,
26+
@Nullable Integer queriesForwarded, @Nullable Integer queriesCached, @Nullable Integer clientsEverSeen,
27+
@Nullable Integer uniqueClients, @Nullable Integer dnsQueriesAllTypes,
28+
@SerializedName("reply_UNKNOWN") @Nullable Integer replyUnknown,
29+
@SerializedName("reply_NODATA") @Nullable Integer replyNoData,
30+
@SerializedName("reply_NXDOMAIN") @Nullable Integer replyNXDomain,
31+
@SerializedName("reply_CNAME") @Nullable Integer replyCName,
32+
@SerializedName("reply_IP") @Nullable Integer replyIP,
33+
@SerializedName("reply_DOMAIN") @Nullable Integer replyDomain,
34+
@SerializedName("reply_RRNAME") @Nullable Integer replyRRName,
35+
@SerializedName("reply_SERVFAIL") @Nullable Integer replyServFail,
36+
@SerializedName("reply_REFUSED") @Nullable Integer replyRefused,
37+
@SerializedName("reply_NOTIMP") @Nullable Integer replyNotImp,
38+
@SerializedName("reply_OTHER") @Nullable Integer replyOther,
39+
@SerializedName("reply_DNSSEC") @Nullable Integer replyDNSSEC,
40+
@SerializedName("reply_NONE") @Nullable Integer replyNone,
41+
@SerializedName("reply_BLOB") @Nullable Integer replyBlob, @Nullable Integer dnsQueriesAllReplies,
42+
@Nullable Integer privacyLevel, @Nullable String status, @Nullable GravityLastUpdated gravityLastUpdated) {
43+
public boolean enabled() {
44+
return "enabled".equalsIgnoreCase(status);
45+
}
46+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/**
2+
* Copyright (c) 2010-2024 Contributors to the openHAB project
3+
*
4+
* See the NOTICE file(s) distributed with this work for additional
5+
* information.
6+
*
7+
* This program and the accompanying materials are made available under the
8+
* terms of the Eclipse Public License 2.0 which is available at
9+
* http://www.eclipse.org/legal/epl-2.0
10+
*
11+
* SPDX-License-Identifier: EPL-2.0
12+
*/
13+
package org.openhab.binding.pihole.internal.rest.model;
14+
15+
import org.eclipse.jdt.annotation.NonNullByDefault;
16+
import org.eclipse.jdt.annotation.Nullable;
17+
18+
/**
19+
* @author Martin Grzeslowski - Initial contribution
20+
*/
21+
@NonNullByDefault
22+
public record GravityLastUpdated(@Nullable Boolean fileExists, @Nullable Long absolute, @Nullable Relative relative) {
23+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/**
2+
* Copyright (c) 2010-2024 Contributors to the openHAB project
3+
*
4+
* See the NOTICE file(s) distributed with this work for additional
5+
* information.
6+
*
7+
* This program and the accompanying materials are made available under the
8+
* terms of the Eclipse Public License 2.0 which is available at
9+
* http://www.eclipse.org/legal/epl-2.0
10+
*
11+
* SPDX-License-Identifier: EPL-2.0
12+
*/
13+
package org.openhab.binding.pihole.internal.rest.model;
14+
15+
import org.eclipse.jdt.annotation.NonNullByDefault;
16+
import org.eclipse.jdt.annotation.Nullable;
17+
18+
/**
19+
* @author Martin Grzeslowski - Initial contribution
20+
*/
21+
@NonNullByDefault
22+
public record Relative(@Nullable Integer days, @Nullable Integer hours, @Nullable Integer minutes) {
23+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<addon:addon id="pihole" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xmlns:addon="https://openhab.org/schemas/addon/v1.0.0"
4+
xsi:schemaLocation="https://openhab.org/schemas/addon/v1.0.0 https://openhab.org/schemas/addon-1.0.0.xsd">
5+
6+
<type>binding</type>
7+
<name>Pi-hole Binding</name>
8+
<description>This is the binding for Pi-hole.</description>
9+
<connection>cloud</connection>
10+
11+
</addon:addon>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
# add-on
2+
3+
addon.pihole.name = Pi-hole Binding
4+
addon.pihole.description = This is the binding for Pi-hole.
5+
6+
# thing types
7+
8+
thing-type.pihole.server.label = Pi-hole Server
9+
thing-type.pihole.server.description = This thing represents a Pi-hole server and is used for the Pi-hole binding.
10+
thing-type.pihole.server.channel.ads-blocked-today.label = Ads Blocked Today
11+
thing-type.pihole.server.channel.ads-blocked-today.description = The number of ads blocked today.
12+
thing-type.pihole.server.channel.ads-percentage-today.label = Ads Percentage Today
13+
thing-type.pihole.server.channel.ads-percentage-today.description = The percentage of ads blocked today.
14+
thing-type.pihole.server.channel.clients-ever-seen.label = Clients Ever Seen
15+
thing-type.pihole.server.channel.clients-ever-seen.description = The total number of unique clients ever seen.
16+
thing-type.pihole.server.channel.dns-queries-all-replies.label = DNS Queries (All Replies)
17+
thing-type.pihole.server.channel.dns-queries-all-replies.description = The total number of DNS queries with all reply types.
18+
thing-type.pihole.server.channel.dns-queries-all-types.label = DNS Queries (All Types)
19+
thing-type.pihole.server.channel.dns-queries-all-types.description = The total number of DNS queries of all types.
20+
thing-type.pihole.server.channel.dns-queries-today.label = DNS Queries Today
21+
thing-type.pihole.server.channel.dns-queries-today.description = The count of DNS queries made today.
22+
thing-type.pihole.server.channel.domains-being-blocked.label = Domains Blocked
23+
thing-type.pihole.server.channel.domains-being-blocked.description = The total number of domains currently being blocked.
24+
thing-type.pihole.server.channel.privacy-level.label = Privacy Level
25+
thing-type.pihole.server.channel.privacy-level.description = The privacy level setting.
26+
thing-type.pihole.server.channel.queries-cached.label = Queries Cached
27+
thing-type.pihole.server.channel.queries-cached.description = The number of queries served from the cache.
28+
thing-type.pihole.server.channel.queries-forwarded.label = Queries Forwarded
29+
thing-type.pihole.server.channel.queries-forwarded.description = The number of queries forwarded to an external DNS server.
30+
thing-type.pihole.server.channel.reply-blob.label = Reply BLOB
31+
thing-type.pihole.server.channel.reply-blob.description = DNS replies with a BLOB (binary large object).
32+
thing-type.pihole.server.channel.reply-cname.label = Reply CNAME
33+
thing-type.pihole.server.channel.reply-cname.description = DNS replies with a CNAME record.
34+
thing-type.pihole.server.channel.reply-dnssec.label = Reply DNSSEC
35+
thing-type.pihole.server.channel.reply-dnssec.description = DNS replies with DNSSEC information.
36+
thing-type.pihole.server.channel.reply-domain.label = Reply DOMAIN
37+
thing-type.pihole.server.channel.reply-domain.description = DNS replies with a domain name.
38+
thing-type.pihole.server.channel.reply-ip.label = Reply IP
39+
thing-type.pihole.server.channel.reply-ip.description = DNS replies with an IP address.
40+
thing-type.pihole.server.channel.reply-nodata.label = Reply NODATA
41+
thing-type.pihole.server.channel.reply-nodata.description = DNS replies indicating no data.
42+
thing-type.pihole.server.channel.reply-none.label = Reply NONE
43+
thing-type.pihole.server.channel.reply-none.description = DNS replies with no data.
44+
thing-type.pihole.server.channel.reply-notimp.label = Reply NOTIMP
45+
thing-type.pihole.server.channel.reply-notimp.description = DNS replies indicating not implemented.
46+
thing-type.pihole.server.channel.reply-nxdomain.label = Reply NXDOMAIN
47+
thing-type.pihole.server.channel.reply-nxdomain.description = DNS replies indicating non-existent domain.
48+
thing-type.pihole.server.channel.reply-other.label = Reply OTHER
49+
thing-type.pihole.server.channel.reply-other.description = DNS replies with other statuses.
50+
thing-type.pihole.server.channel.reply-refused.label = Reply REFUSED
51+
thing-type.pihole.server.channel.reply-refused.description = DNS replies indicating refusal.
52+
thing-type.pihole.server.channel.reply-rrname.label = Reply RRNAME
53+
thing-type.pihole.server.channel.reply-rrname.description = DNS replies with a resource record name.
54+
thing-type.pihole.server.channel.reply-servfail.label = Reply SERVFAIL
55+
thing-type.pihole.server.channel.reply-servfail.description = DNS replies indicating a server failure.
56+
thing-type.pihole.server.channel.reply-unknown.label = Reply UNKNOWN
57+
thing-type.pihole.server.channel.reply-unknown.description = DNS replies with an unknown status.
58+
thing-type.pihole.server.channel.unique-clients.label = Unique Clients
59+
thing-type.pihole.server.channel.unique-clients.description = The current count of unique clients.
60+
thing-type.pihole.server.channel.unique-domains.label = Unique Domains
61+
thing-type.pihole.server.channel.unique-domains.description = The count of unique domains queried.
62+
63+
# thing types config
64+
65+
thing-type.config.pihole.server.hostname.label = Hostname
66+
thing-type.config.pihole.server.hostname.description = Hostname or IP address of the device
67+
thing-type.config.pihole.server.refreshIntervalSeconds.label = Refresh Interval
68+
thing-type.config.pihole.server.refreshIntervalSeconds.description = Interval the device is polled in sec.
69+
thing-type.config.pihole.server.token.label = Token
70+
thing-type.config.pihole.server.token.description = Token to access the device. To generate token go to `settings` > `API` > `Show API token`
71+
72+
# channel types
73+
74+
channel-type.pihole.disable-enable-channel.label = Disable Blocking
75+
channel-type.pihole.disable-enable-channel.command.option.DISABLE = Disable Blocking Indefinitely
76+
channel-type.pihole.disable-enable-channel.command.option.FOR_10_SEC = Disable Blocking for 10 seconds
77+
channel-type.pihole.disable-enable-channel.command.option.FOR_30_SEC = Disable Blocking for 30 seconds
78+
channel-type.pihole.disable-enable-channel.command.option.FOR_5_MIN = Disable Blocking for 5 minutes
79+
channel-type.pihole.disable-enable-channel.command.option.ENABLE = Enable Blocking
80+
channel-type.pihole.enabled-channel.label = Status
81+
channel-type.pihole.enabled-channel.description = The current status of blocking
82+
channel-type.pihole.number-channel.label = Number channel
83+
84+
# channel types
85+
86+
channel.ads_blocked_today.label = Ads Blocked Today
87+
channel.ads_blocked_today.description = The number of ads blocked today.
88+
channel.ads_percentage_today.label = Ads Percentage Today
89+
channel.ads_percentage_today.description = The percentage of ads blocked today.
90+
channel.clients_ever_seen.label = Clients Ever Seen
91+
channel.clients_ever_seen.description = The total number of unique clients ever seen.
92+
channel.disable-enable.label = Disable Blocking
93+
channel.disable-enable.description = Commands to disable or enable blocking.
94+
channel.disable-enable.command.DISABLE = Disable Blocking Indefinitely
95+
channel.disable-enable.command.FOR_10_SEC = Disable Blocking for 10 seconds
96+
channel.disable-enable.command.FOR_30_SEC = Disable Blocking for 30 seconds
97+
channel.disable-enable.command.FOR_5_MIN = Disable Blocking for 5 minutes
98+
channel.disable-enable.command.ENABLE = Enable Blocking
99+
channel.dns_queries_all_replies.label = DNS Queries (All Replies)
100+
channel.dns_queries_all_replies.description = The total number of DNS queries with all reply types.
101+
channel.dns_queries_all_types.label = DNS Queries (All Types)
102+
channel.dns_queries_all_types.description = The total number of DNS queries of all types.
103+
channel.dns_queries_today.label = DNS Queries Today
104+
channel.dns_queries_today.description = The count of DNS queries made today.
105+
channel.domains_being_blocked.label = Domains Blocked
106+
channel.domains_being_blocked.description = The total number of domains currently being blocked.
107+
channel.enabled.label = Enabled
108+
channel.enabled.description = The current status of blocking.
109+
channel.privacy_level.label = Privacy Level
110+
channel.privacy_level.description = The privacy level setting.
111+
channel.queries_cached.label = Queries Cached
112+
channel.queries_cached.description = The number of queries served from the cache.
113+
channel.queries_forwarded.label = Queries Forwarded
114+
channel.queries_forwarded.description = The number of queries forwarded to an external DNS server.
115+
channel.reply_BLOB.label = Reply BLOB
116+
channel.reply_BLOB.description = DNS replies with a BLOB (binary large object).
117+
channel.reply_CNAME.label = Reply CNAME
118+
channel.reply_CNAME.description = DNS replies with a CNAME record.
119+
channel.reply_DNSSEC.label = Reply DNSSEC
120+
channel.reply_DNSSEC.description = DNS replies with DNSSEC information.
121+
channel.reply_DOMAIN.label = Reply DOMAIN
122+
channel.reply_DOMAIN.description = DNS replies with a domain name.
123+
channel.reply_IP.label = Reply IP
124+
channel.reply_IP.description = DNS replies with an IP address.
125+
channel.reply_NODATA.label = Reply NODATA
126+
channel.reply_NODATA.description = DNS replies indicating no data.
127+
channel.reply_NONE.label = Reply NONE
128+
channel.reply_NONE.description = DNS replies with no data.
129+
channel.reply_NOTIMP.label = Reply NOTIMP
130+
channel.reply_NOTIMP.description = DNS replies indicating not implemented.
131+
channel.reply_NXDOMAIN.label = Reply NXDOMAIN
132+
channel.reply_NXDOMAIN.description = DNS replies indicating non-existent domain.
133+
channel.reply_OTHER.label = Reply OTHER
134+
channel.reply_OTHER.description = DNS replies with other statuses.
135+
channel.reply_REFUSED.label = Reply REFUSED
136+
channel.reply_REFUSED.description = DNS replies indicating refusal.
137+
channel.reply_RRNAME.label = Reply RRNAME
138+
channel.reply_RRNAME.description = DNS replies with a resource record name.
139+
channel.reply_SERVFAIL.label = Reply SERVFAIL
140+
channel.reply_SERVFAIL.description = DNS replies indicating a server failure.
141+
channel.reply_UNKNOWN.label = Reply UNKNOWN
142+
channel.reply_UNKNOWN.description = DNS replies with an unknown status.
143+
channel.unique_clients.label = Unique Clients
144+
channel.unique_clients.description = The current count of unique clients.
145+
channel.unique_domains.label = Unique Domains
146+
channel.unique_domains.description = The count of unique domains queried.
147+
thing.server.label = Pi-hole Binding Thing
148+
thing.server.description = Sample thing for Pi-hole Binding
149+
150+
# action
151+
152+
action.disable.label = Disable blocking ads
153+
action.disable.description = Temporarily stop blocking advertisements.
154+
action.disableInf.label = Disable blocking ads (for infinity)
155+
action.disableInf.description = Stop blocking advertisements.
156+
action.disable.timeLabel = Duration
157+
action.disable.timeDescription = Specify the time for which ad blocking should be disabled (e.g., "for 30 minutes").
158+
action.disable.timeUnitLabel = Time Unit
159+
action.disable.timeUnitDescription = The unit of time for the specified duration.
160+
action.enable.label = Enable blocking ads
161+
action.enable.description = Resume blocking advertisements.
162+
163+
# from code
164+
165+
handler.init.wrongInterval = Refresh interval needs to be greater than 0!
166+
handler.init.noToken = Please provide token
167+
handler.init.invalidHostname = Invalid hostname "{0}"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<thing:thing-descriptions bindingId="pihole"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
5+
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
6+
7+
<thing-type id="server">
8+
<label>Pi-hole Server</label>
9+
<description>This thing represents a Pi-hole server and is used for the Pi-hole binding.</description>
10+
11+
<channels>
12+
<channel id="domains-being-blocked" typeId="number-channel">
13+
<label>Domains Blocked</label>
14+
<description>The total number of domains currently being blocked.</description>
15+
</channel>
16+
<channel id="dns-queries-today" typeId="number-channel">
17+
<label>DNS Queries Today</label>
18+
<description>The count of DNS queries made today.</description>
19+
</channel>
20+
<channel id="ads-blocked-today" typeId="number-channel">
21+
<label>Ads Blocked Today</label>
22+
<description>The number of ads blocked today.</description>
23+
</channel>
24+
<channel id="ads-percentage-today" typeId="number-channel">
25+
<label>Ads Percentage Today</label>
26+
<description>The percentage of ads blocked today.</description>
27+
</channel>
28+
<channel id="unique-domains" typeId="number-channel">
29+
<label>Unique Domains</label>
30+
<description>The count of unique domains queried.</description>
31+
</channel>
32+
<channel id="queries-forwarded" typeId="number-channel">
33+
<label>Queries Forwarded</label>
34+
<description>The number of queries forwarded to an external DNS server.</description>
35+
</channel>
36+
<channel id="queries-cached" typeId="number-channel">
37+
<label>Queries Cached</label>
38+
<description>The number of queries served from the cache.</description>
39+
</channel>
40+
<channel id="clients-ever-seen" typeId="number-channel">
41+
<label>Clients Ever Seen</label>
42+
<description>The total number of unique clients ever seen.</description>
43+
</channel>
44+
<channel id="unique-clients" typeId="number-channel">
45+
<label>Unique Clients</label>
46+
<description>The current count of unique clients.</description>
47+
</channel>
48+
<channel id="dns-queries-all-types" typeId="number-channel">
49+
<label>DNS Queries (All Types)</label>
50+
<description>The total number of DNS queries of all types.</description>
51+
</channel>
52+
<channel id="reply-unknown" typeId="number-channel">
53+
<label>Reply UNKNOWN</label>
54+
<description>DNS replies with an unknown status.</description>
55+
</channel>
56+
<channel id="reply-nodata" typeId="number-channel">
57+
<label>Reply NODATA</label>
58+
<description>DNS replies indicating no data.</description>
59+
</channel>
60+
<channel id="reply-nxdomain" typeId="number-channel">
61+
<label>Reply NXDOMAIN</label>
62+
<description>DNS replies indicating non-existent domain.</description>
63+
</channel>
64+
<channel id="reply-cname" typeId="number-channel">
65+
<label>Reply CNAME</label>
66+
<description>DNS replies with a CNAME record.</description>
67+
</channel>
68+
<channel id="reply-ip" typeId="number-channel">
69+
<label>Reply IP</label>
70+
<description>DNS replies with an IP address.</description>
71+
</channel>
72+
<channel id="reply-domain" typeId="number-channel">
73+
<label>Reply DOMAIN</label>
74+
<description>DNS replies with a domain name.</description>
75+
</channel>
76+
<channel id="reply-rrname" typeId="number-channel">
77+
<label>Reply RRNAME</label>
78+
<description>DNS replies with a resource record name.</description>
79+
</channel>
80+
<channel id="reply-servfail" typeId="number-channel">
81+
<label>Reply SERVFAIL</label>
82+
<description>DNS replies indicating a server failure.</description>
83+
</channel>
84+
<channel id="reply-refused" typeId="number-channel">
85+
<label>Reply REFUSED</label>
86+
<description>DNS replies indicating refusal.</description>
87+
</channel>
88+
<channel id="reply-notimp" typeId="number-channel">
89+
<label>Reply NOTIMP</label>
90+
<description>DNS replies indicating not implemented.</description>
91+
</channel>
92+
<channel id="reply-other" typeId="number-channel">
93+
<label>Reply OTHER</label>
94+
<description>DNS replies with other statuses.</description>
95+
</channel>
96+
<channel id="reply-dnssec" typeId="number-channel">
97+
<label>Reply DNSSEC</label>
98+
<description>DNS replies with DNSSEC information.</description>
99+
</channel>
100+
<channel id="reply-none" typeId="number-channel">
101+
<label>Reply NONE</label>
102+
<description>DNS replies with no data.</description>
103+
</channel>
104+
<channel id="reply-blob" typeId="number-channel">
105+
<label>Reply BLOB</label>
106+
<description>DNS replies with a BLOB (binary large object).</description>
107+
</channel>
108+
<channel id="dns-queries-all-replies" typeId="number-channel">
109+
<label>DNS Queries (All Replies)</label>
110+
<description>The total number of DNS queries with all reply types.</description>
111+
</channel>
112+
<channel id="privacy-level" typeId="number-channel">
113+
<label>Privacy Level</label>
114+
<description>The privacy level setting.</description>
115+
</channel>
116+
<channel id="enabled" typeId="enabled-channel"/>
117+
<channel id="disable-enable" typeId="disable-enable-channel"/>
118+
</channels>
119+
120+
<config-description>
121+
<parameter name="hostname" type="text" required="true">
122+
<context>network-address</context>
123+
<label>Hostname</label>
124+
<description>Hostname or IP address of the device</description>
125+
</parameter>
126+
<parameter name="token" type="text" required="true">
127+
<context>password</context>
128+
<label>Token</label>
129+
<description>Token to access the device. To generate token go to `settings` > `API` > `Show API token`</description>
130+
</parameter>
131+
<parameter name="refreshIntervalSeconds" type="integer" unit="s" min="1">
132+
<label>Refresh Interval</label>
133+
<description>Interval the device is polled in sec.</description>
134+
<default>600</default>
135+
<advanced>true</advanced>
136+
</parameter>
137+
</config-description>
138+
</thing-type>
139+
140+
<channel-type id="number-channel">
141+
<item-type>Number</item-type>
142+
<label>Number channel</label>
143+
<state readOnly="true"/>
144+
</channel-type>
145+
<channel-type id="enabled-channel">
146+
<item-type>Switch</item-type>
147+
<label>Status</label>
148+
<description>The current status of blocking</description>
149+
<state readOnly="true"/>
150+
</channel-type>
151+
<channel-type id="disable-enable-channel">
152+
<item-type>String</item-type>
153+
<label>Disable Blocking</label>
154+
<command>
155+
<options>
156+
<option value="DISABLE">Disable Blocking Indefinitely</option>
157+
<option value="FOR_10_SEC">Disable Blocking for 10 seconds</option>
158+
<option value="FOR_30_SEC">Disable Blocking for 30 seconds</option>
159+
<option value="FOR_5_MIN">Disable Blocking for 5 minutes</option>
160+
<option value="ENABLE">Enable Blocking</option>
161+
</options>
162+
</command>
163+
</channel-type>
164+
</thing:thing-descriptions>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
/**
2+
* Copyright (c) 2010-2024 Contributors to the openHAB project
3+
*
4+
* See the NOTICE file(s) distributed with this work for additional
5+
* information.
6+
*
7+
* This program and the accompanying materials are made available under the
8+
* terms of the Eclipse Public License 2.0 which is available at
9+
* http://www.eclipse.org/legal/epl-2.0
10+
*
11+
* SPDX-License-Identifier: EPL-2.0
12+
*/
13+
package org.openhab.binding.pihole.internal.rest;
14+
15+
import static java.util.concurrent.TimeUnit.SECONDS;
16+
import static org.assertj.core.api.Assertions.assertThat;
17+
import static org.mockito.BDDMockito.given;
18+
import static org.mockito.Mockito.mock;
19+
20+
import java.net.URI;
21+
22+
import org.eclipse.jdt.annotation.NonNullByDefault;
23+
import org.eclipse.jetty.client.HttpClient;
24+
import org.eclipse.jetty.client.api.ContentResponse;
25+
import org.eclipse.jetty.client.api.Request;
26+
import org.junit.jupiter.api.DisplayName;
27+
import org.junit.jupiter.api.Test;
28+
import org.openhab.binding.pihole.internal.rest.model.DnsStatistics;
29+
import org.openhab.binding.pihole.internal.rest.model.GravityLastUpdated;
30+
import org.openhab.binding.pihole.internal.rest.model.Relative;
31+
32+
/**
33+
* @author Martin Grzeslowski - Initial contribution
34+
*/
35+
@NonNullByDefault
36+
public class JettyAdminServiceTest {
37+
String content = """
38+
{
39+
"domains_being_blocked": 131355,
40+
"dns_queries_today": 27459,
41+
"ads_blocked_today": 2603,
42+
"ads_percentage_today": 9.479588,
43+
"unique_domains": 6249,
44+
"queries_forwarded": 16030,
45+
"queries_cached": 8525,
46+
"clients_ever_seen": 2,
47+
"unique_clients": 2,
48+
"dns_queries_all_types": 27459,
49+
"reply_UNKNOWN": 631,
50+
"reply_NODATA": 3168,
51+
"reply_NXDOMAIN": 492,
52+
"reply_CNAME": 9819,
53+
"reply_IP": 13224,
54+
"reply_DOMAIN": 48,
55+
"reply_RRNAME": 0,
56+
"reply_SERVFAIL": 0,
57+
"reply_REFUSED": 0,
58+
"reply_NOTIMP": 0,
59+
"reply_OTHER": 0,
60+
"reply_DNSSEC": 0,
61+
"reply_NONE": 0,
62+
"reply_BLOB": 77,
63+
"dns_queries_all_replies": 27459,
64+
"privacy_level": 0,
65+
"status": "enabled",
66+
"gravity_last_updated": {
67+
"file_exists": true,
68+
"absolute": 1712457841,
69+
"relative": {
70+
"days": 0,
71+
"hours": 7,
72+
"minutes": 3
73+
}
74+
}
75+
}
76+
""";
77+
78+
// Returns a DnsStatistics object when called with valid token and baseUrl
79+
@Test
80+
@DisplayName("Returns a DnsStatistics object when called with valid token and baseUrl")
81+
public void testReturnsDnsStatisticsObjectWithValidTokenAndBaseUrl() throws Exception {
82+
// Given
83+
var token = "validToken";
84+
var baseUrl = URI.create("https://example.com");
85+
var client = mock(HttpClient.class);
86+
var adminService = new JettyAdminService(token, baseUrl, client);
87+
var dnsStatistics = new DnsStatistics(131355, // domains_being_blocked
88+
27459, // dns_queries_today
89+
2603, // ads_blocked_today
90+
9.479588, // ads_percentage_today
91+
6249, // unique_domains
92+
16030, // queries_forwarded
93+
8525, // queries_cached
94+
2, // clients_ever_seen
95+
2, // unique_clients
96+
27459, // dns_queries_all_types
97+
631, // reply_UNKNOWN
98+
3168, // reply_NODATA
99+
492, // reply_NXDOMAIN
100+
9819, // reply_CNAME
101+
13224, // reply_IP
102+
48, // reply_DOMAIN
103+
0, // reply_RRNAME
104+
0, // reply_SERVFAIL
105+
0, // reply_REFUSED
106+
0, // reply_NOTIMP
107+
0, // reply_OTHER
108+
0, // reply_DNSSEC
109+
0, // reply_NONE
110+
77, // reply_BLOB
111+
27459, // dns_queries_all_replies
112+
0, // privacy_level
113+
"enabled", // status
114+
new GravityLastUpdated(true, 1712457841L, new Relative(0, 7, 3)));
115+
var response = mock(ContentResponse.class);
116+
var request = mock(Request.class);
117+
given(request.timeout(10, SECONDS)).willReturn(request);
118+
119+
given(client.newRequest(URI.create("https://example.com/admin/api.php?summaryRaw&auth=validToken")))
120+
.willReturn(request);
121+
given(request.send()).willReturn(response);
122+
given(response.getContentAsString()).willReturn(content);
123+
124+
// When
125+
var result = adminService.summary();
126+
127+
// Then
128+
assertThat(result).contains(dnsStatistics);
129+
}
130+
}

‎bundles/pom.xml

+1
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,7 @@
316316
<module>org.openhab.binding.pegelonline</module>
317317
<module>org.openhab.binding.pentair</module>
318318
<module>org.openhab.binding.phc</module>
319+
<module>org.openhab.binding.pihole</module>
319320
<module>org.openhab.binding.pilight</module>
320321
<module>org.openhab.binding.pioneeravr</module>
321322
<module>org.openhab.binding.pixometer</module>

0 commit comments

Comments
 (0)
Please sign in to comment.