[pihole] Integration of API v6 (#19350)

* Initiating integration of API v6

Signed-off-by: clinique <gael@lhopital.org>

* Adds enable / disable blocking

Signed-off-by: clinique <gael@lhopital.org>

* Correction of warnings

Signed-off-by: clinique <gael@lhopital.org>

* Finalizing API v6 for translated values of the handler.
Code review corrections.

Signed-off-by: gael@lhopital.org <gael@lhopital.org>

* Adding missing Relative field.

Signed-off-by: gael@lhopital.org <gael@lhopital.org>

---------

Signed-off-by: clinique <gael@lhopital.org>
Signed-off-by: gael@lhopital.org <gael@lhopital.org>
This commit is contained in:
Gaël L'hopital
2025-11-01 10:23:49 +01:00
committed by GitHub
parent a3fef83f4d
commit 4bded900e4
19 changed files with 692 additions and 49 deletions
+3 -2
View File
@@ -22,9 +22,10 @@ The Pi-hole Binding allows you to monitor Pi-hole statistics and control its fun
| Name | Type | Description | Default | Required | Advanced | | Name | Type | Description | Default | Required | Advanced |
|-----------------|---------|-------------------------------------------------------------------------------------------|---------|----------|----------| |-----------------|---------|-------------------------------------------------------------------------------------------|---------|----------|----------|
| hostname | text | Hostname or IP address of the device | N/A | yes | no | | hostname | text | URL (hostname or IP address) of the pihole web server | 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 | | 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 | | refreshInterval | integer | Interval the device is polled in sec | 600 | no | yes |
| serverVersion | text | Defines the API to be used with this server | v5 | no | no |
## Channels ## Channels
@@ -53,15 +53,11 @@ public class PiHoleActions implements ThingActions {
return; return;
} }
if (timeUnit == null) {
timeUnit = SECONDS;
}
var local = handler; var local = handler;
if (local == null) { if (local == null) {
return; return;
} }
local.disableBlocking(timeUnit.toSeconds(time)); local.disableBlocking((timeUnit == null ? SECONDS : timeUnit).toSeconds(time));
} }
public static void disableBlocking(@Nullable ThingActions actions, long time, @Nullable TimeUnit timeUnit) public static void disableBlocking(@Nullable ThingActions actions, long time, @Nullable TimeUnit timeUnit)
@@ -21,7 +21,11 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
*/ */
@NonNullByDefault @NonNullByDefault
public class PiHoleConfiguration { public class PiHoleConfiguration {
public static final String API_V6 = "v6";
public static final String API_V5 = "v5";
public String hostname = ""; public String hostname = "";
public String token = ""; public String token = "";
public int refreshIntervalSeconds = 600; public int refreshIntervalSeconds = 600;
public String serverVersion = API_V5;
} }
@@ -33,8 +33,10 @@ import java.util.concurrent.ScheduledFuture;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.DisableEnable;
import org.openhab.binding.pihole.internal.rest.AdminService; import org.openhab.binding.pihole.internal.rest.AdminService;
import org.openhab.binding.pihole.internal.rest.JettyAdminService; import org.openhab.binding.pihole.internal.rest.JettyAdminService;
import org.openhab.binding.pihole.internal.rest.JettyAdminServiceV6;
import org.openhab.binding.pihole.internal.rest.model.DnsStatistics; import org.openhab.binding.pihole.internal.rest.model.DnsStatistics;
import org.openhab.core.i18n.TimeZoneProvider; import org.openhab.core.i18n.TimeZoneProvider;
import org.openhab.core.library.types.DateTimeType; import org.openhab.core.library.types.DateTimeType;
@@ -51,6 +53,8 @@ import org.openhab.core.types.RefreshType;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
/** /**
* The {@link PiHoleHandler} is responsible for handling commands, which are * The {@link PiHoleHandler} is responsible for handling commands, which are
* sent to one of the channels. * sent to one of the channels.
@@ -58,21 +62,23 @@ import org.slf4j.LoggerFactory;
* @author Martin Grzeslowski - Initial contribution * @author Martin Grzeslowski - Initial contribution
*/ */
@NonNullByDefault @NonNullByDefault
public class PiHoleHandler extends BaseThingHandler implements AdminService { public class PiHoleHandler extends BaseThingHandler {
private static final int HTTP_DELAY_SECONDS = 1; private static final int HTTP_DELAY_SECONDS = 1;
private final Logger logger = LoggerFactory.getLogger(PiHoleHandler.class); private final Logger logger = LoggerFactory.getLogger(PiHoleHandler.class);
private final Object lock = new Object(); private final Object lock = new Object();
private final TimeZoneProvider timeZoneProvider; private final TimeZoneProvider timeZoneProvider;
private final HttpClient httpClient; private final HttpClient httpClient;
private final Gson gson;
private @Nullable AdminService adminService; private @Nullable AdminService adminService;
private @Nullable DnsStatistics dnsStatistics; private @Nullable DnsStatistics dnsStatistics;
private @Nullable ScheduledFuture<?> scheduledFuture; private @Nullable ScheduledFuture<?> scheduledFuture;
public PiHoleHandler(Thing thing, TimeZoneProvider timeZoneProvider, HttpClient httpClient) { public PiHoleHandler(Thing thing, TimeZoneProvider timeZoneProvider, HttpClient httpClient, Gson gson) {
super(thing); super(thing);
this.timeZoneProvider = timeZoneProvider; this.timeZoneProvider = timeZoneProvider;
this.httpClient = httpClient; this.httpClient = httpClient;
this.gson = gson;
} }
@Override @Override
@@ -101,7 +107,11 @@ public class PiHoleHandler extends BaseThingHandler implements AdminService {
updateStatus(OFFLINE, CONFIGURATION_ERROR, "@text/handler.init.noToken"); updateStatus(OFFLINE, CONFIGURATION_ERROR, "@text/handler.init.noToken");
return; return;
} }
adminService = new JettyAdminService(config.token, hostname, httpClient);
adminService = PiHoleConfiguration.API_V6.equals(config.serverVersion)
? new JettyAdminServiceV6(config.token, hostname, httpClient, gson)
: new JettyAdminService(config.token, hostname, httpClient, gson);
scheduledFuture = scheduler.scheduleWithFixedDelay(this::update, 0, config.refreshIntervalSeconds, SECONDS); scheduledFuture = scheduler.scheduleWithFixedDelay(this::update, 0, config.refreshIntervalSeconds, SECONDS);
// do not set status here, the background task will do it. // do not set status here, the background task will do it.
@@ -234,7 +244,6 @@ public class PiHoleHandler extends BaseThingHandler implements AdminService {
super.dispose(); super.dispose();
} }
@Override
public Optional<DnsStatistics> summary() throws PiHoleException { public Optional<DnsStatistics> summary() throws PiHoleException {
var local = adminService; var local = adminService;
if (local == null) { if (local == null) {
@@ -243,7 +252,6 @@ public class PiHoleHandler extends BaseThingHandler implements AdminService {
return local.summary(); return local.summary();
} }
@Override
public void disableBlocking(long seconds) throws PiHoleException { public void disableBlocking(long seconds) throws PiHoleException {
var local = adminService; var local = adminService;
if (local == null) { if (local == null) {
@@ -259,7 +267,6 @@ public class PiHoleHandler extends BaseThingHandler implements AdminService {
} }
} }
@Override
public void enableBlocking() throws PiHoleException { public void enableBlocking() throws PiHoleException {
var local = adminService; var local = adminService;
if (local == null) { if (local == null) {
@@ -29,6 +29,10 @@ import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference; import org.osgi.service.component.annotations.Reference;
import com.google.gson.FieldNamingPolicy;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
/** /**
* The {@link PiHoleHandlerFactory} is responsible for creating things and thing * The {@link PiHoleHandlerFactory} is responsible for creating things and thing
* handlers. * handlers.
@@ -38,8 +42,10 @@ import org.osgi.service.component.annotations.Reference;
@NonNullByDefault @NonNullByDefault
@Component(configurationPid = "binding.pihole", service = ThingHandlerFactory.class) @Component(configurationPid = "binding.pihole", service = ThingHandlerFactory.class)
public class PiHoleHandlerFactory extends BaseThingHandlerFactory { public class PiHoleHandlerFactory extends BaseThingHandlerFactory {
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(PI_HOLE_TYPE); private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(PI_HOLE_TYPE);
private static final Gson GSON = new GsonBuilder()
.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create();
private final TimeZoneProvider timeZoneProvider; private final TimeZoneProvider timeZoneProvider;
private final HttpClientFactory httpClientFactory; private final HttpClientFactory httpClientFactory;
@@ -60,9 +66,8 @@ public class PiHoleHandlerFactory extends BaseThingHandlerFactory {
ThingTypeUID thingTypeUID = thing.getThingTypeUID(); ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (PI_HOLE_TYPE.equals(thingTypeUID)) { if (PI_HOLE_TYPE.equals(thingTypeUID)) {
return new PiHoleHandler(thing, timeZoneProvider, httpClientFactory.getCommonHttpClient()); return new PiHoleHandler(thing, timeZoneProvider, httpClientFactory.getCommonHttpClient(), GSON);
} }
return null; return null;
} }
} }
@@ -13,23 +13,41 @@
package org.openhab.binding.pihole.internal.rest; package org.openhab.binding.pihole.internal.rest;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
import org.eclipse.jdt.annotation.NonNullByDefault; 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.PiHoleException;
import org.openhab.binding.pihole.internal.rest.model.DnsStatistics; import org.openhab.binding.pihole.internal.rest.model.DnsStatistics;
import com.google.gson.Gson;
/** /**
* @author Martin Grzeslowski - Initial contribution * @author Martin Grzeslowski - Initial contribution
* @author Gaël L'hopital - Changed from 'interface' to abstract class
*/ */
@NonNullByDefault @NonNullByDefault
public interface AdminService { public abstract class AdminService {
protected static final long TIMEOUT_SECONDS = 10L;
protected final HttpClient client;
protected final Gson gson;
public AdminService(HttpClient client, Gson gson) {
this.client = client;
this.gson = gson;
}
/** /**
* Retrieves a summary of DNS statistics. * Retrieves a summary of DNS statistics.
* *
* @return An optional containing the DNS statistics. * @return An optional containing the DNS statistics.
* @throws PiHoleException In case of error * @throws PiHoleException In case of error
*/ */
Optional<DnsStatistics> summary() throws PiHoleException; public abstract Optional<DnsStatistics> summary() throws PiHoleException;
/** /**
* Disables blocking for a specified duration. * Disables blocking for a specified duration.
@@ -37,12 +55,21 @@ public interface AdminService {
* @param seconds The duration in seconds for which blocking should be disabled. * @param seconds The duration in seconds for which blocking should be disabled.
* @throws PiHoleException In case of error * @throws PiHoleException In case of error
*/ */
void disableBlocking(long seconds) throws PiHoleException; public abstract void disableBlocking(long seconds) throws PiHoleException;
/** /**
* Enables blocking. * Enables blocking.
* *
* @throws PiHoleException In case of error * @throws PiHoleException In case of error
*/ */
void enableBlocking() throws PiHoleException; public abstract void enableBlocking() throws PiHoleException;
protected 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);
}
}
} }
@@ -16,39 +16,30 @@ import static java.util.concurrent.TimeUnit.SECONDS;
import java.net.URI; import java.net.URI;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.client.HttpClient; 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.PiHoleException;
import org.openhab.binding.pihole.internal.rest.model.DnsStatistics; import org.openhab.binding.pihole.internal.rest.model.DnsStatistics;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import com.google.gson.FieldNamingPolicy;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
/** /**
* @author Martin Grzeslowski - Initial contribution * @author Martin Grzeslowski - Initial contribution
*/ */
@NonNullByDefault @NonNullByDefault
public class JettyAdminService implements AdminService { public class JettyAdminService extends 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 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) { private final URI baseUrl;
this.token = token; private final String token;
public JettyAdminService(String token, URI baseUrl, HttpClient client, Gson gson) {
super(client, gson);
this.baseUrl = baseUrl; this.baseUrl = baseUrl;
this.client = client; this.token = token;
} }
@Override @Override
@@ -58,16 +49,7 @@ public class JettyAdminService implements AdminService {
var request = client.newRequest(url).timeout(TIMEOUT_SECONDS, SECONDS); var request = client.newRequest(url).timeout(TIMEOUT_SECONDS, SECONDS);
var response = send(request); var response = send(request);
var content = response.getContentAsString(); var content = response.getContentAsString();
return Optional.ofNullable(GSON.fromJson(content, DnsStatistics.class)); 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 @Override
@@ -0,0 +1,193 @@
/*
* Copyright (c) 2010-2025 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.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Optional;
import javax.ws.rs.core.UriBuilder;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.util.StringContentProvider;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
import org.openhab.binding.pihole.internal.PiHoleException;
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;
import org.openhab.binding.pihole.internal.rest.model.v6.Blocking;
import org.openhab.binding.pihole.internal.rest.model.v6.ConfigAnswer;
import org.openhab.binding.pihole.internal.rest.model.v6.DnsBlockingAnswer;
import org.openhab.binding.pihole.internal.rest.model.v6.HistoryClients;
import org.openhab.binding.pihole.internal.rest.model.v6.Password;
import org.openhab.binding.pihole.internal.rest.model.v6.SessionAnswer;
import org.openhab.binding.pihole.internal.rest.model.v6.SessionAnswer.Session;
import org.openhab.binding.pihole.internal.rest.model.v6.StatAnswer;
import org.openhab.binding.pihole.internal.rest.model.v6.StatAnswer.Gravity;
import org.openhab.binding.pihole.internal.rest.model.v6.StatAnswer.Queries;
import org.openhab.binding.pihole.internal.rest.model.v6.StatAnswer.Replies;
import org.openhab.binding.pihole.internal.rest.model.v6.StatDatabaseSummary;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
/**
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public class JettyAdminServiceV6 extends AdminService {
private final Logger logger = LoggerFactory.getLogger(JettyAdminServiceV6.class);
private final URI authURI;
private final URI statsSummaryURI;
private final URI dnsBlockingURI;
private final URI databaseSummaryURI;
private final URI historyClientsURI;
private final URI configURI;
private final String tokenJson;
private @Nullable String sid;
private @Nullable Instant sessionValidity;
private @Nullable Instant midnightUtc;
public JettyAdminServiceV6(String token, URI baseUrl, HttpClient client, Gson gson) {
super(client, gson);
tokenJson = gson.toJson(new Password(token));
UriBuilder apiUriBuilder = UriBuilder.fromUri(baseUrl).path("api");
authURI = apiUriBuilder.clone().path("auth").build();
configURI = apiUriBuilder.clone().path("config").build();
dnsBlockingURI = apiUriBuilder.clone().path("dns").path("blocking").build();
historyClientsURI = apiUriBuilder.clone().path("history").path("clients").build();
UriBuilder statsUriBuilder = apiUriBuilder.clone().path("stats");
statsSummaryURI = statsUriBuilder.clone().path("summary").build();
databaseSummaryURI = statsUriBuilder.clone().path("database").path("summary").build();
}
private String updateSid() throws PiHoleException {
logger.debug("Get or update session ID");
var request = client.newRequest(authURI).timeout(TIMEOUT_SECONDS, SECONDS).method(HttpMethod.POST)
.header(HttpHeader.ACCEPT, "application/json").content(new StringContentProvider(tokenJson));
var content = send(request).getContentAsString();
if (gson.fromJson(content, SessionAnswer.class) instanceof SessionAnswer answer) {
Session session = answer.session();
sid = session.sid();
sessionValidity = Instant.now().plusSeconds(session.cautiousValidity());
return session.sid();
}
throw new PiHoleException("Error deserializing '%s'".formatted(content));
}
private String getSid() throws PiHoleException {
if (sid instanceof String localSid && sessionValidity instanceof Instant validUntil
&& Instant.now().isBefore(validUntil)) {
return localSid;
}
return updateSid();
}
private long getTodayMidnight() {
Instant now = Instant.now();
Instant local = midnightUtc;
if (local == null || now.minus(1, ChronoUnit.DAYS).isAfter(local)) {
local = now.truncatedTo(ChronoUnit.DAYS);
midnightUtc = local;
}
return local.getEpochSecond();
}
@Override
public Optional<DnsStatistics> summary() throws PiHoleException {
logger.debug("Building the as if it was a v5 API");
StatAnswer statAnswer = get(statsSummaryURI, StatAnswer.class);
Gravity gravity = statAnswer.gravity();
Queries statQueries = statAnswer.queries();
Replies replies = statQueries.replies();
DnsBlockingAnswer blockingAnswer = get(dnsBlockingURI, DnsBlockingAnswer.class);
long todayMidnight = getTodayMidnight();
StatDatabaseSummary statDatabase = get(databaseSummaryURI, StatDatabaseSummary.class, "from",
Long.toString(todayMidnight), "until", Long.toString(todayMidnight + 24 * 60 * 60));
HistoryClients historyClients = get(historyClientsURI, HistoryClients.class, "N", "0");
ConfigAnswer configAnswer = get(configURI, ConfigAnswer.class);
Duration duration = Duration.between(gravity.instant(), Instant.now());
Relative relative = new Relative((int) duration.toDaysPart(), duration.toHoursPart(), duration.toMinutesPart());
DnsStatistics translated = new DnsStatistics(gravity.domainsBeingBlocked(), statDatabase.sumQueries(),
statDatabase.sumBlocked(), statDatabase.percentBlocked(), statQueries.uniqueDomains(),
statQueries.forwarded(), statQueries.cached(), historyClients.clients().size(), null,
statQueries.types().all(), replies.unknown(), replies.nodata(), replies.nxdomain(), replies.cname(),
replies.ip(), replies.domain(), replies.rrname(), replies.servfail(), replies.refused(),
replies.notimp(), replies.other(), replies.dnssec(), replies.none(), replies.blob(), replies.all(),
configAnswer.config().misc().privacylevel(), blockingAnswer.blocking(), new GravityLastUpdated(
configAnswer.config().files().gravity() != null, gravity.lastUpdate(), relative));
return Optional.of(translated);
}
@Override
public void disableBlocking(long seconds) throws PiHoleException {
logger.debug("Disabling blocking for {} seconds", seconds);
post(dnsBlockingURI, new Blocking(false, seconds));
}
@Override
public void enableBlocking() throws PiHoleException {
logger.debug("Enabling blocking");
post(dnsBlockingURI, Blocking.BLOCK);
}
private void post(URI targetURI, Object object) throws PiHoleException {
var request = client.newRequest(targetURI).timeout(TIMEOUT_SECONDS, SECONDS).method(HttpMethod.POST)
.header(HttpHeader.ACCEPT, "application/json").header("sid", getSid())
.content(new StringContentProvider(gson.toJson(object)));
send(request);
}
private <T> T get(URI targetURI, Class<T> clazz, @Nullable Object... params) throws PiHoleException {
var request = client.newRequest(targetURI).timeout(TIMEOUT_SECONDS, SECONDS)
.header(HttpHeader.ACCEPT, "application/json").header("sid", getSid());
if (params.length % 2 != 0) {
throw new IllegalArgumentException("params count must be even");
}
for (int i = 0; i < params.length; i += 2) {
if (params[i] instanceof String name && params[i + 1] instanceof String param) {
request = request.param(name, param);
} else {
throw new IllegalArgumentException("parameters must be String");
}
}
var content = send(request).getContentAsString();
T answer = gson.fromJson(content, clazz);
if (answer != null) {
return answer;
}
throw new PiHoleException("Error deserializing %s".formatted(content));
}
}
@@ -0,0 +1,24 @@
/*
* Copyright (c) 2010-2025 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.v6;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public record Blocking(boolean blocking, @Nullable Long timer) {
public static Blocking BLOCK = new Blocking(true, null);
}
@@ -0,0 +1,132 @@
/*
* Copyright (c) 2010-2025 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.v6;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public record ConfigAnswer(Config config, double took) {
public record Temp(int limit, String unit) {
}
public record Api(int maxSessions, boolean prettyJSON, String pwhash, String password, String totpSecret,
String appPwhash, boolean appSudo, boolean cliPw, List<Object> excludeClients, List<Object> excludeDomains,
int maxHistory, int maxClients, boolean clientHistoryGlobalMax, boolean allowDestructive, Temp temp) {
}
public record Host(boolean force4, String iPv4, boolean force6, String iPv6) {
}
public record Blocking(boolean active, String mode, String edns) {
}
public record Blocking_1(boolean force4, String iPv4, boolean force6, String iPv6) {
}
public record Cache(int size, int optimizer, int upstreamBlockedTTL) {
}
public record Network(boolean parseARPcache, int expire) {
}
public record Database(boolean dBimport, int maxDBdays, int dBinterval, boolean useWAL, Network network) {
}
public record Debug(boolean database, boolean networking, boolean locks, boolean queries, boolean flags,
boolean shmem, boolean gc, boolean arp, boolean regex, boolean api, boolean tls, boolean overtime,
boolean status, boolean caps, boolean dnssec, boolean vectors, boolean resolver, boolean edns0,
boolean clients, boolean aliasclients, boolean events, boolean helper, boolean config, boolean inotify,
boolean webserver, boolean extra, boolean reserved, boolean ntp, boolean netlink, boolean all) {
}
public record Dhcp(boolean active, String start, String end, String router, String netmask, String leaseTime,
boolean ipv6, boolean rapidCommit, boolean multiDNS, boolean logging, boolean ignoreUnknownClients,
List<String> hosts) {
}
public record SpecialDomains(boolean mozillaCanary, boolean iCloudPrivateRelay, boolean designatedResolver) {
}
public record Reply(Host host, Blocking_1 blocking) {
}
public record RateLimit(int count, int interval) {
}
public record Dns(List<String> upstreams, boolean cNAMEdeepInspect, boolean blockESNI, boolean edns0ecs,
boolean ignoreLocalhost, boolean showDNSSEC, boolean analyzeOnlyAandAAAA, String piholePTR,
String replyWhenBusy, int blockTTL, List<String> hosts, boolean domainNeeded, boolean expandHosts,
String domain, boolean bogusPriv, boolean dnssec, String _interface, String hostRecord,
String listeningMode, boolean queryLogging, List<Object> cnameRecords, int port, List<Object> revServers,
Cache cache, Blocking blocking, SpecialDomains specialDomains, Reply reply, RateLimit rateLimit) {
}
public record Files(String pid, String database, @Nullable String gravity, String gravityTmp, String macvendor,
String pcap, Log log) {
}
public record Interface(boolean boxed, String theme) {
}
public record Ipv4(boolean active, String address) {
}
public record Ipv6(boolean active, String address) {
}
public record Log(String ftl, String dnsmasq, String webserver) {
}
public record Check(boolean load, int shmem, int disk) {
}
public record Misc(int privacylevel, int delayStartup, int nice, boolean addr2line, boolean etcDnsmasqD,
List<Object> dnsmasqLines, boolean extraLogging, boolean readOnly, Check check) {
}
public record Rtc(boolean set, String device, boolean utc) {
}
public record Sync(boolean active, String server, int interval, int count, Rtc rtc) {
}
public record Ntp(Ipv4 ipv4, Ipv6 ipv6, Sync sync) {
}
public record Paths(String webroot, String webhome, String prefix) {
}
public record Resolver(boolean resolveIPv4, boolean resolveIPv6, boolean networkNames, String refreshNames) {
}
public record Session(int timeout, boolean restore) {
}
public record Tls(String cert) {
}
public record Webserver(String domain, String acl, String port, int threads, List<String> headers, boolean serveAll,
Session session, Tls tls, Paths paths, Interface _interface, Api api) {
}
public record Config(Dns dns, Dhcp dhcp, Ntp ntp, Resolver resolver, Database database, Webserver webserver,
Files files, Misc misc, Debug debug) {
}
}
@@ -0,0 +1,22 @@
/*
* Copyright (c) 2010-2025 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.v6;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public record DnsBlockingAnswer(String blocking, Double timer, double took) {
}
@@ -0,0 +1,25 @@
/*
* Copyright (c) 2010-2025 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.v6;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public record HistoryAnswer(History history, double took) {
public record History(double timestamp, int total, int cached, int blocked, int forwarded) {
}
}
@@ -0,0 +1,32 @@
/*
* Copyright (c) 2010-2025 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.v6;
import java.util.List;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public record HistoryClients(Map<String, Client> clients, List<History> history, double took) {
public record Client(String name, int total) {
}
public record History(double timestamp, Map<String, Integer> data) {
}
}
@@ -0,0 +1,22 @@
/*
* Copyright (c) 2010-2025 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.v6;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public record Password(String password) {
}
@@ -0,0 +1,27 @@
/*
* Copyright (c) 2010-2025 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.v6;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public record SessionAnswer(Session session, double took) {
public record Session(boolean valid, boolean totp, String sid, String csrf, int validity, String message) {
public int cautiousValidity() {
return validity / 3 * 2;
}
}
}
@@ -0,0 +1,105 @@
/*
* Copyright (c) 2010-2025 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.v6;
import java.time.Instant;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.google.gson.annotations.SerializedName;
/**
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public record StatAnswer(Queries queries, Clients client, Gravity gravity, double took) {
record Clients(int active, int total) {
}
public record Gravity(int domainsBeingBlocked, long lastUpdate) {
public Instant instant() {
return Instant.ofEpochSecond(lastUpdate);
}
}
public record Queries(int total, int blocked, double percentBlocked, int uniqueDomains, int forwarded, int cached,
double frequency, Types types, Status status, Replies replies) {
}
public record Replies(@SerializedName("UNKNOWN") int unknown, //
@SerializedName("NODATA") int nodata, //
@SerializedName("NXDOMAIN") int nxdomain, //
@SerializedName("CNAME") int cname, //
@SerializedName("IP") int ip, //
@SerializedName("DOMAIN") int domain, //
@SerializedName("RRNAME") int rrname, //
@SerializedName("SERVFAIL") int servfail, //
@SerializedName("REFUSED") int refused, //
@SerializedName("NOTIMP") int notimp, //
@SerializedName("OTHER") int other, //
@SerializedName("DNSSEC") int dnssec, //
@SerializedName("NONE") int none, //
@SerializedName("BLOB") int blob) {
public int all() {
return unknown + nodata + nxdomain + cname + ip + domain + rrname + servfail + refused + notimp + other
+ dnssec + none + blob;
}
}
public record Status(@SerializedName("UNKNOWN") int unknown, //
@SerializedName("GRAVITY") int gravity, //
@SerializedName("FORWARDED") int forwarded, //
@SerializedName("CACHE") int cache, //
@SerializedName("REGEX") int regex, //
@SerializedName("DENYLIST") int denylist, //
@SerializedName("EXTERNAL_BLOCKED_IP") int externalBlockedIp, //
@SerializedName("EXTERNAL_BLOCKED_NULL") int externalBlockedNull, //
@SerializedName("EXTERNAL_BLOCKED_NXRA") int externalBlockedNxra, //
@SerializedName("GRAVITY_CNAME") int gravityCname, //
@SerializedName("REGEX_CNAME") int regexCname, //
@SerializedName("DENYLIST_CNAME") int denylistCname, //
@SerializedName("RETRIED") int retried, //
@SerializedName("RETRIED_DNSSEC") int retriedDnssec, //
@SerializedName("IN_PROGRESS") int inProgress, //
@SerializedName("DBBUSY") int dbbusy, //
@SerializedName("SPECIAL_DOMAIN") int specialDomain, //
@SerializedName("CACHE_STALE") int cacheStale, //
@SerializedName("EXTERNAL_BLOCKED_EDE15") int externalBlockedEde15) {
}
public record Types(@SerializedName("A") int a, //
@SerializedName("AAAA") int aaaa, //
@SerializedName("ANY") int any, //
@SerializedName("SRV") int srv, //
@SerializedName("SOA") int soa, //
@SerializedName("PTR") int ptr, //
@SerializedName("TXT") int txt, //
@SerializedName("NAPTR") int naptr, //
@SerializedName("MX") int mx, //
@SerializedName("DS") int ds, //
@SerializedName("RRSIG") int rrsig, //
@SerializedName("DNSKEY") int dnskey, //
@SerializedName("NS") int ns, //
@SerializedName("SVCB") int svcb, //
@SerializedName("HTTPS") int https, //
@SerializedName("OTHER") int other) {
public int all() {
return a + aaaa + any + srv + soa + ptr + txt + naptr + mx + ds + rrsig + dnskey + ns + svcb + https
+ other;
}
}
}
@@ -0,0 +1,23 @@
/*
* Copyright (c) 2010-2025 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.v6;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public record StatDatabaseSummary(int sumQueries, int sumBlocked, double percentBlocked, int totalClients,
double took) {
}
@@ -123,8 +123,8 @@
<config-description> <config-description>
<parameter name="hostname" type="text" required="true"> <parameter name="hostname" type="text" required="true">
<context>network-address</context> <context>network-address</context>
<label>Hostname</label> <label>Server URL</label>
<description>Hostname or IP address of the device</description> <description>URL (hostname or IP address) of the pihole web server</description>
</parameter> </parameter>
<parameter name="token" type="text" required="true"> <parameter name="token" type="text" required="true">
<context>password</context> <context>password</context>
@@ -137,6 +137,15 @@
<default>600</default> <default>600</default>
<advanced>true</advanced> <advanced>true</advanced>
</parameter> </parameter>
<parameter name="serverVersion" type="text">
<label>Server Version</label>
<description>Defines the API to be used with this server.</description>
<default>v5</default>
<options>
<option value="v5">v5</option>
<option value="v6">v6</option>
</options>
</parameter>
</config-description> </config-description>
</thing-type> </thing-type>
@@ -29,11 +29,18 @@ 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.GravityLastUpdated;
import org.openhab.binding.pihole.internal.rest.model.Relative; import org.openhab.binding.pihole.internal.rest.model.Relative;
import com.google.gson.FieldNamingPolicy;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
/** /**
* @author Martin Grzeslowski - Initial contribution * @author Martin Grzeslowski - Initial contribution
*/ */
@NonNullByDefault @NonNullByDefault
public class JettyAdminServiceTest { public class JettyAdminServiceTest {
private static final Gson GSON = new GsonBuilder()
.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create();
String content = """ String content = """
{ {
"domains_being_blocked": 131355, "domains_being_blocked": 131355,
@@ -83,7 +90,7 @@ public class JettyAdminServiceTest {
var token = "validToken"; var token = "validToken";
var baseUrl = URI.create("https://example.com"); var baseUrl = URI.create("https://example.com");
var client = mock(HttpClient.class); var client = mock(HttpClient.class);
var adminService = new JettyAdminService(token, baseUrl, client); var adminService = new JettyAdminService(token, baseUrl, client, GSON);
var dnsStatistics = new DnsStatistics(131355, // domains_being_blocked var dnsStatistics = new DnsStatistics(131355, // domains_being_blocked
27459, // dns_queries_today 27459, // dns_queries_today
2603, // ads_blocked_today 2603, // ads_blocked_today