From 8841ce88fa6a2050357094a34aea1524e517210f Mon Sep 17 00:00:00 2001 From: clinique Date: Thu, 18 Sep 2025 14:44:44 +0200 Subject: [PATCH 1/5] Initiating integration of API v6 Signed-off-by: clinique --- bundles/org.openhab.binding.pihole/README.md | 5 +- .../pihole/internal/PiHoleActions.java | 6 +- .../pihole/internal/PiHoleConfiguration.java | 4 + .../pihole/internal/PiHoleHandler.java | 19 ++-- .../pihole/internal/PiHoleHandlerFactory.java | 10 +- .../pihole/internal/rest/AdminService.java | 43 +++++++-- .../internal/rest/JettyAdminService.java | 30 +----- .../internal/rest/JettyAdminServiceV6.java | 96 +++++++++++++++++++ .../internal/rest/model/v6/Password.java | 22 +++++ .../internal/rest/model/v6/SessionAnswer.java | 25 +++++ .../internal/rest/model/v6/StatAnswer.java | 82 ++++++++++++++++ .../resources/OH-INF/thing/thing-types.xml | 14 ++- .../internal/rest/JettyAdminServiceTest.java | 9 +- 13 files changed, 316 insertions(+), 49 deletions(-) create mode 100644 bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/JettyAdminServiceV6.java create mode 100644 bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/model/v6/Password.java create mode 100644 bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/model/v6/SessionAnswer.java create mode 100644 bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/model/v6/StatAnswer.java diff --git a/bundles/org.openhab.binding.pihole/README.md b/bundles/org.openhab.binding.pihole/README.md index d07593c3a8e55..f21840aabb3da 100644 --- a/bundles/org.openhab.binding.pihole/README.md +++ b/bundles/org.openhab.binding.pihole/README.md @@ -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 | |-----------------|---------|-------------------------------------------------------------------------------------------|---------|----------|----------| -| 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 | -| 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 | yes | ## Channels diff --git a/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/PiHoleActions.java b/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/PiHoleActions.java index 0b63f0e45e1e2..ced04025ba23f 100644 --- a/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/PiHoleActions.java +++ b/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/PiHoleActions.java @@ -53,15 +53,11 @@ public void disableBlocking( return; } - if (timeUnit == null) { - timeUnit = SECONDS; - } - var local = handler; if (local == null) { 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) diff --git a/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/PiHoleConfiguration.java b/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/PiHoleConfiguration.java index b7f65b315dfa0..763d5b3ff944f 100644 --- a/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/PiHoleConfiguration.java +++ b/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/PiHoleConfiguration.java @@ -21,7 +21,11 @@ */ @NonNullByDefault public class PiHoleConfiguration { + public static final String API_V6 = "v6"; + public static final String API_V5 = "v5"; + public String hostname = ""; public String token = ""; public int refreshIntervalSeconds = 600; + public String serverVersion = API_V5; } diff --git a/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/PiHoleHandler.java b/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/PiHoleHandler.java index f2f790ca8a1c5..ea11fcc001a0e 100644 --- a/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/PiHoleHandler.java +++ b/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/PiHoleHandler.java @@ -33,8 +33,10 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; 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.JettyAdminService; +import org.openhab.binding.pihole.internal.rest.JettyAdminServiceV6; import org.openhab.binding.pihole.internal.rest.model.DnsStatistics; import org.openhab.core.i18n.TimeZoneProvider; import org.openhab.core.library.types.DateTimeType; @@ -51,6 +53,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.google.gson.Gson; + /** * The {@link PiHoleHandler} is responsible for handling commands, which are * sent to one of the channels. @@ -58,21 +62,23 @@ * @author Martin Grzeslowski - Initial contribution */ @NonNullByDefault -public class PiHoleHandler extends BaseThingHandler implements AdminService { +public class PiHoleHandler extends BaseThingHandler { private static final int HTTP_DELAY_SECONDS = 1; private final Logger logger = LoggerFactory.getLogger(PiHoleHandler.class); private final Object lock = new Object(); private final TimeZoneProvider timeZoneProvider; private final HttpClient httpClient; + private final Gson gson; private @Nullable AdminService adminService; private @Nullable DnsStatistics dnsStatistics; private @Nullable ScheduledFuture scheduledFuture; - public PiHoleHandler(Thing thing, TimeZoneProvider timeZoneProvider, HttpClient httpClient) { + public PiHoleHandler(Thing thing, TimeZoneProvider timeZoneProvider, HttpClient httpClient, Gson gson) { super(thing); this.timeZoneProvider = timeZoneProvider; this.httpClient = httpClient; + this.gson = gson; } @Override @@ -101,7 +107,11 @@ public void initialize() { updateStatus(OFFLINE, CONFIGURATION_ERROR, "@text/handler.init.noToken"); 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); // do not set status here, the background task will do it. @@ -234,7 +244,6 @@ public void dispose() { super.dispose(); } - @Override public Optional summary() throws PiHoleException { var local = adminService; if (local == null) { @@ -243,7 +252,6 @@ public Optional summary() throws PiHoleException { return local.summary(); } - @Override public void disableBlocking(long seconds) throws PiHoleException { var local = adminService; if (local == null) { @@ -259,7 +267,6 @@ public void disableBlocking(long seconds) throws PiHoleException { } } - @Override public void enableBlocking() throws PiHoleException { var local = adminService; if (local == null) { diff --git a/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/PiHoleHandlerFactory.java b/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/PiHoleHandlerFactory.java index 42647fd73b4ed..8db2673de776a 100644 --- a/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/PiHoleHandlerFactory.java +++ b/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/PiHoleHandlerFactory.java @@ -29,6 +29,10 @@ import org.osgi.service.component.annotations.Component; 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 * handlers. @@ -38,8 +42,10 @@ @NonNullByDefault @Component(configurationPid = "binding.pihole", service = ThingHandlerFactory.class) public class PiHoleHandlerFactory extends BaseThingHandlerFactory { - private static final Set 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 HttpClientFactory httpClientFactory; @@ -60,7 +66,7 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) { ThingTypeUID thingTypeUID = thing.getThingTypeUID(); if (PI_HOLE_TYPE.equals(thingTypeUID)) { - return new PiHoleHandler(thing, timeZoneProvider, httpClientFactory.getCommonHttpClient()); + return new PiHoleHandler(thing, timeZoneProvider, httpClientFactory.getCommonHttpClient(), GSON); } return null; diff --git a/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/AdminService.java b/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/AdminService.java index b197b0b48f5d7..56862a59894ac 100644 --- a/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/AdminService.java +++ b/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/AdminService.java @@ -13,23 +13,44 @@ package org.openhab.binding.pihole.internal.rest; import java.util.Optional; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.api.Request; import org.openhab.binding.pihole.internal.PiHoleException; import org.openhab.binding.pihole.internal.rest.model.DnsStatistics; +import com.google.gson.Gson; + /** - * @author Martin Grzeslowski - Initial contribution + * @author Gaël L'hopital - Initial contribution */ @NonNullByDefault -public interface AdminService { +public abstract class AdminService { + protected static final long TIMEOUT_SECONDS = 10L; + + protected final String token; + + protected final HttpClient client; + protected final Gson gson; + + public AdminService(String token, HttpClient client, Gson gson) { + super(); + this.token = token; + this.client = client; + this.gson = gson; + } + /** * Retrieves a summary of DNS statistics. * * @return An optional containing the DNS statistics. * @throws PiHoleException In case of error */ - Optional summary() throws PiHoleException; + public abstract Optional summary() throws PiHoleException; /** * Disables blocking for a specified duration. @@ -37,12 +58,22 @@ public interface AdminService { * @param seconds The duration in seconds for which blocking should be disabled. * @throws PiHoleException In case of error */ - void disableBlocking(long seconds) throws PiHoleException; + public abstract void disableBlocking(long seconds) throws PiHoleException; /** * Enables blocking. * * @throws PiHoleException In case of error */ - void enableBlocking() throws PiHoleException; -} + public abstract void enableBlocking() throws PiHoleException; + + protected 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); + } + } + +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/JettyAdminService.java b/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/JettyAdminService.java index 1dade2803f013..a52db509286a8 100644 --- a/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/JettyAdminService.java +++ b/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/JettyAdminService.java @@ -16,39 +16,28 @@ import java.net.URI; import java.util.Optional; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeoutException; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jetty.client.HttpClient; -import org.eclipse.jetty.client.api.ContentResponse; -import org.eclipse.jetty.client.api.Request; import org.openhab.binding.pihole.internal.PiHoleException; import org.openhab.binding.pihole.internal.rest.model.DnsStatistics; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.google.gson.FieldNamingPolicy; import com.google.gson.Gson; -import com.google.gson.GsonBuilder; /** * @author Martin Grzeslowski - Initial contribution */ @NonNullByDefault -public class JettyAdminService implements AdminService { - private static final Gson GSON = new GsonBuilder() - .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create(); - private static final long TIMEOUT_SECONDS = 10L; +public class JettyAdminService extends AdminService { private final Logger logger = LoggerFactory.getLogger(JettyAdminService.class); - private final String token; + private final URI baseUrl; - private final HttpClient client; - public JettyAdminService(String token, URI baseUrl, HttpClient client) { - this.token = token; + public JettyAdminService(String token, URI baseUrl, HttpClient client, Gson gson) { + super(token, client, gson); this.baseUrl = baseUrl; - this.client = client; } @Override @@ -58,16 +47,7 @@ public Optional summary() throws PiHoleException { var request = client.newRequest(url).timeout(TIMEOUT_SECONDS, SECONDS); var response = send(request); var content = response.getContentAsString(); - return Optional.ofNullable(GSON.fromJson(content, DnsStatistics.class)); - } - - private static ContentResponse send(Request request) throws PiHoleException { - try { - return request.send(); - } catch (InterruptedException | TimeoutException | ExecutionException e) { - throw new PiHoleException( - "Exception while sending request to Pi-hole. %s".formatted(e.getLocalizedMessage()), e); - } + return Optional.ofNullable(gson.fromJson(content, DnsStatistics.class)); } @Override diff --git a/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/JettyAdminServiceV6.java b/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/JettyAdminServiceV6.java new file mode 100644 index 0000000000000..65c7381bd8332 --- /dev/null +++ b/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/JettyAdminServiceV6.java @@ -0,0 +1,96 @@ +/* + * 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.util.Optional; + +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.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.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.Gson; + +/** + * @author Martin Grzeslowski - Initial contribution + */ +@NonNullByDefault +public class JettyAdminServiceV6 extends AdminService { + private final Logger logger = LoggerFactory.getLogger(JettyAdminServiceV6.class); + protected final URI apiUrl; + private @Nullable Session session; + + public JettyAdminServiceV6(String token, URI baseUrl, HttpClient client, Gson gson) { + super(token, client, gson); + apiUrl = baseUrl.resolve("/api"); + } + + private void getAuth() throws PiHoleException { + logger.debug("Check if authentication is required"); + var authUrl = apiUrl.resolve(apiUrl.getPath() + "/auth"); + var request = client.newRequest(authUrl).timeout(TIMEOUT_SECONDS, SECONDS).method(HttpMethod.POST) + .header(HttpHeader.ACCEPT, "application/json") + .content(new StringContentProvider(gson.toJson(new Password(token)))); + var response = send(request); + var content = response.getContentAsString(); + SessionAnswer answer = gson.fromJson(content, SessionAnswer.class); + logger.debug(answer.session().message()); + session = answer.session(); + } + + @Override + public Optional summary() throws PiHoleException { + logger.debug("Getting summary"); + getAuth(); + var url = apiUrl.resolve(apiUrl.getPath() + "/stats/summary"); + var request = client.newRequest(url).timeout(TIMEOUT_SECONDS, SECONDS).method(HttpMethod.GET) + .header(HttpHeader.ACCEPT, "application/json").header("sid", session.sid()); + var response = send(request); + var content = response.getContentAsString(); + StatAnswer answer = gson.fromJson(content, StatAnswer.class); + DnsStatistics translated = new DnsStatistics(answer.gravity().domainsBeingBlocked(), null, null, null, + answer.queries().uniqueDomains(), answer.queries().forwarded(), answer.queries().cached(), null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null); + return Optional.of(translated); + } + + @Override + public void disableBlocking(long seconds) throws PiHoleException { + logger.debug("Disabling blocking for {} seconds", seconds); + // var url = baseUrl.resolve("/admin/api.php?disable=%s&auth=%s".formatted(seconds, token)); + // var request = client.newRequest(url).timeout(TIMEOUT_SECONDS, SECONDS); + // send(request); + } + + @Override + public void enableBlocking() throws PiHoleException { + logger.debug("Enabling blocking"); + // var url = baseUrl.resolve("/admin/api.php?enable&auth=%s".formatted(token)); + // var request = client.newRequest(url).timeout(TIMEOUT_SECONDS, SECONDS); + // send(request); + } +} diff --git a/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/model/v6/Password.java b/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/model/v6/Password.java new file mode 100644 index 0000000000000..9246058470e4a --- /dev/null +++ b/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/model/v6/Password.java @@ -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) { +} diff --git a/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/model/v6/SessionAnswer.java b/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/model/v6/SessionAnswer.java new file mode 100644 index 0000000000000..750ad34f99dfb --- /dev/null +++ b/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/model/v6/SessionAnswer.java @@ -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 SessionAnswer(Session session, double took) { + public record Session(boolean valid, boolean totp, String sid, String csrf, int validity, String message) { + + } +} diff --git a/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/model/v6/StatAnswer.java b/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/model/v6/StatAnswer.java new file mode 100644 index 0000000000000..ab21858cfab51 --- /dev/null +++ b/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/model/v6/StatAnswer.java @@ -0,0 +1,82 @@ +/* + * 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 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 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, int 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 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) { + } + +} diff --git a/bundles/org.openhab.binding.pihole/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.pihole/src/main/resources/OH-INF/thing/thing-types.xml index 27949e316bd54..19a9b8b0142cf 100644 --- a/bundles/org.openhab.binding.pihole/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.pihole/src/main/resources/OH-INF/thing/thing-types.xml @@ -123,8 +123,8 @@ network-address - - Hostname or IP address of the device + + Url (hostname or IP address) of the pihole web server password @@ -137,6 +137,16 @@ 600 true + + + Defines the API to be used with this server. + v5 + true + + + + + diff --git a/bundles/org.openhab.binding.pihole/src/test/java/org/openhab/binding/pihole/internal/rest/JettyAdminServiceTest.java b/bundles/org.openhab.binding.pihole/src/test/java/org/openhab/binding/pihole/internal/rest/JettyAdminServiceTest.java index df3caed50947e..6e4f9b96d5309 100644 --- a/bundles/org.openhab.binding.pihole/src/test/java/org/openhab/binding/pihole/internal/rest/JettyAdminServiceTest.java +++ b/bundles/org.openhab.binding.pihole/src/test/java/org/openhab/binding/pihole/internal/rest/JettyAdminServiceTest.java @@ -29,11 +29,18 @@ import org.openhab.binding.pihole.internal.rest.model.GravityLastUpdated; 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 */ @NonNullByDefault public class JettyAdminServiceTest { + private static final Gson GSON = new GsonBuilder() + .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create(); + String content = """ { "domains_being_blocked": 131355, @@ -83,7 +90,7 @@ public void testReturnsDnsStatisticsObjectWithValidTokenAndBaseUrl() throws Exce var token = "validToken"; var baseUrl = URI.create("https://example.com"); 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 27459, // dns_queries_today 2603, // ads_blocked_today From d429e35a47699d66e32c5307e239428d82e3bef1 Mon Sep 17 00:00:00 2001 From: clinique Date: Thu, 18 Sep 2025 17:07:27 +0200 Subject: [PATCH 2/5] Adds enable / disable blocking Signed-off-by: clinique --- .../pihole/internal/rest/AdminService.java | 6 +-- .../internal/rest/JettyAdminServiceV6.java | 46 +++++++++++++------ .../internal/rest/model/v6/Blocking.java | 24 ++++++++++ .../rest/model/v6/DnsBlockingAnswer.java | 22 +++++++++ .../internal/rest/model/v6/StatAnswer.java | 31 ++++++++++--- .../resources/OH-INF/thing/thing-types.xml | 20 ++++---- 6 files changed, 116 insertions(+), 33 deletions(-) create mode 100644 bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/model/v6/Blocking.java create mode 100644 bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/model/v6/DnsBlockingAnswer.java diff --git a/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/AdminService.java b/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/AdminService.java index 56862a59894ac..1bdc06564efab 100644 --- a/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/AdminService.java +++ b/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/AdminService.java @@ -26,7 +26,8 @@ import com.google.gson.Gson; /** - * @author Gaël L'hopital - Initial contribution + * @author Martin Grzeslowski - Initial contribution + * @author Gaël L'hopital - Changed from 'interface' to abstract class */ @NonNullByDefault public abstract class AdminService { @@ -75,5 +76,4 @@ protected static ContentResponse send(Request request) throws PiHoleException { "Exception while sending request to Pi-hole. %s".formatted(e.getLocalizedMessage()), e); } } - -} \ No newline at end of file +} diff --git a/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/JettyAdminServiceV6.java b/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/JettyAdminServiceV6.java index 65c7381bd8332..dd7b42c7102cb 100644 --- a/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/JettyAdminServiceV6.java +++ b/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/JettyAdminServiceV6.java @@ -25,17 +25,21 @@ 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.v6.Blocking; +import org.openhab.binding.pihole.internal.rest.model.v6.DnsBlockingAnswer; 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.Queries; +import org.openhab.binding.pihole.internal.rest.model.v6.StatAnswer.Replies; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.gson.Gson; /** - * @author Martin Grzeslowski - Initial contribution + * @author Gaël L'hopital - Initial contribution */ @NonNullByDefault public class JettyAdminServiceV6 extends AdminService { @@ -66,31 +70,45 @@ public Optional summary() throws PiHoleException { logger.debug("Getting summary"); getAuth(); var url = apiUrl.resolve(apiUrl.getPath() + "/stats/summary"); - var request = client.newRequest(url).timeout(TIMEOUT_SECONDS, SECONDS).method(HttpMethod.GET) + var request = client.newRequest(url).timeout(TIMEOUT_SECONDS, SECONDS) .header(HttpHeader.ACCEPT, "application/json").header("sid", session.sid()); var response = send(request); - var content = response.getContentAsString(); - StatAnswer answer = gson.fromJson(content, StatAnswer.class); - DnsStatistics translated = new DnsStatistics(answer.gravity().domainsBeingBlocked(), null, null, null, - answer.queries().uniqueDomains(), answer.queries().forwarded(), answer.queries().cached(), null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null); + StatAnswer statAnswer = gson.fromJson(response.getContentAsString(), StatAnswer.class); + + url = apiUrl.resolve(apiUrl.getPath() + "/dns/blocking"); + request = client.newRequest(url).timeout(TIMEOUT_SECONDS, SECONDS).header(HttpHeader.ACCEPT, "application/json") + .header("sid", session.sid()); + response = send(request); + DnsBlockingAnswer blockingAnswer = gson.fromJson(response.getContentAsString(), DnsBlockingAnswer.class); + + Queries queries = statAnswer.queries(); + Replies replies = queries.replies(); + DnsStatistics translated = new DnsStatistics(statAnswer.gravity().domainsBeingBlocked(), null, null, null, + queries.uniqueDomains(), queries.forwarded(), queries.cached(), null, null, queries.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(), null, + blockingAnswer.blocking(), null); return Optional.of(translated); } @Override public void disableBlocking(long seconds) throws PiHoleException { logger.debug("Disabling blocking for {} seconds", seconds); - // var url = baseUrl.resolve("/admin/api.php?disable=%s&auth=%s".formatted(seconds, token)); - // var request = client.newRequest(url).timeout(TIMEOUT_SECONDS, SECONDS); - // send(request); + internalBlock(new Blocking(false, seconds)); } @Override public void enableBlocking() throws PiHoleException { logger.debug("Enabling blocking"); - // var url = baseUrl.resolve("/admin/api.php?enable&auth=%s".formatted(token)); - // var request = client.newRequest(url).timeout(TIMEOUT_SECONDS, SECONDS); - // send(request); + internalBlock(Blocking.BLOCK); + } + + private void internalBlock(Blocking action) throws PiHoleException { + var url = apiUrl.resolve(apiUrl.getPath() + "/dns/blocking"); + var request = client.newRequest(url).timeout(TIMEOUT_SECONDS, SECONDS).method(HttpMethod.POST) + .header(HttpHeader.ACCEPT, "application/json").header("sid", session.sid()) + .content(new StringContentProvider(gson.toJson(action))); + send(request); } } diff --git a/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/model/v6/Blocking.java b/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/model/v6/Blocking.java new file mode 100644 index 0000000000000..394c528f5ad4f --- /dev/null +++ b/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/model/v6/Blocking.java @@ -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); +} diff --git a/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/model/v6/DnsBlockingAnswer.java b/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/model/v6/DnsBlockingAnswer.java new file mode 100644 index 0000000000000..de1b1077b0618 --- /dev/null +++ b/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/model/v6/DnsBlockingAnswer.java @@ -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 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) { +} diff --git a/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/model/v6/StatAnswer.java b/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/model/v6/StatAnswer.java index ab21858cfab51..f97fe6a6bf024 100644 --- a/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/model/v6/StatAnswer.java +++ b/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/model/v6/StatAnswer.java @@ -48,6 +48,11 @@ public record Replies(@SerializedName("UNKNOWN") int unknown, // @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, // @@ -71,12 +76,26 @@ public record Status(@SerializedName("UNKNOWN") int unknown, // @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, + 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; + } + } } diff --git a/bundles/org.openhab.binding.pihole/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.pihole/src/main/resources/OH-INF/thing/thing-types.xml index 19a9b8b0142cf..ddf7145b1605b 100644 --- a/bundles/org.openhab.binding.pihole/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.pihole/src/main/resources/OH-INF/thing/thing-types.xml @@ -137,16 +137,16 @@ 600 true - - - Defines the API to be used with this server. - v5 - true - - - - - + + + Defines the API to be used with this server. + v5 + true + + + + + From 4b8b33160ef5da8225f42af7ba8b6c7f642fb7cd Mon Sep 17 00:00:00 2001 From: clinique Date: Thu, 18 Sep 2025 17:17:25 +0200 Subject: [PATCH 3/5] Correction of warnings Signed-off-by: clinique --- .../pihole/internal/PiHoleHandlerFactory.java | 8 +-- .../internal/rest/JettyAdminServiceV6.java | 61 +++++++++++-------- .../rest/model/v6/DnsBlockingAnswer.java | 2 +- .../internal/rest/model/v6/StatAnswer.java | 2 +- 4 files changed, 39 insertions(+), 34 deletions(-) diff --git a/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/PiHoleHandlerFactory.java b/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/PiHoleHandlerFactory.java index 8db2673de776a..617e841dade8f 100644 --- a/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/PiHoleHandlerFactory.java +++ b/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/PiHoleHandlerFactory.java @@ -65,10 +65,8 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) { protected @Nullable ThingHandler createHandler(Thing thing) { ThingTypeUID thingTypeUID = thing.getThingTypeUID(); - if (PI_HOLE_TYPE.equals(thingTypeUID)) { - return new PiHoleHandler(thing, timeZoneProvider, httpClientFactory.getCommonHttpClient(), GSON); - } - - return null; + return PI_HOLE_TYPE.equals(thingTypeUID) + ? new PiHoleHandler(thing, timeZoneProvider, httpClientFactory.getCommonHttpClient(), GSON) + : null; } } diff --git a/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/JettyAdminServiceV6.java b/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/JettyAdminServiceV6.java index dd7b42c7102cb..0481a85daad7d 100644 --- a/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/JettyAdminServiceV6.java +++ b/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/JettyAdminServiceV6.java @@ -60,36 +60,41 @@ private void getAuth() throws PiHoleException { .content(new StringContentProvider(gson.toJson(new Password(token)))); var response = send(request); var content = response.getContentAsString(); - SessionAnswer answer = gson.fromJson(content, SessionAnswer.class); - logger.debug(answer.session().message()); - session = answer.session(); + if (gson.fromJson(content, SessionAnswer.class) instanceof SessionAnswer answer) { + session = answer.session(); + } } @Override public Optional summary() throws PiHoleException { logger.debug("Getting summary"); getAuth(); - var url = apiUrl.resolve(apiUrl.getPath() + "/stats/summary"); - var request = client.newRequest(url).timeout(TIMEOUT_SECONDS, SECONDS) - .header(HttpHeader.ACCEPT, "application/json").header("sid", session.sid()); - var response = send(request); - StatAnswer statAnswer = gson.fromJson(response.getContentAsString(), StatAnswer.class); + if (session instanceof Session local) { + var url = apiUrl.resolve(apiUrl.getPath() + "/stats/summary"); + var request = client.newRequest(url).timeout(TIMEOUT_SECONDS, SECONDS) + .header(HttpHeader.ACCEPT, "application/json").header("sid", local.sid()); + var response = send(request); + StatAnswer statAnswer = gson.fromJson(response.getContentAsString(), StatAnswer.class); - url = apiUrl.resolve(apiUrl.getPath() + "/dns/blocking"); - request = client.newRequest(url).timeout(TIMEOUT_SECONDS, SECONDS).header(HttpHeader.ACCEPT, "application/json") - .header("sid", session.sid()); - response = send(request); - DnsBlockingAnswer blockingAnswer = gson.fromJson(response.getContentAsString(), DnsBlockingAnswer.class); + url = apiUrl.resolve(apiUrl.getPath() + "/dns/blocking"); + request = client.newRequest(url).timeout(TIMEOUT_SECONDS, SECONDS) + .header(HttpHeader.ACCEPT, "application/json").header("sid", local.sid()); + response = send(request); + DnsBlockingAnswer blockingAnswer = gson.fromJson(response.getContentAsString(), DnsBlockingAnswer.class); - Queries queries = statAnswer.queries(); - Replies replies = queries.replies(); - DnsStatistics translated = new DnsStatistics(statAnswer.gravity().domainsBeingBlocked(), null, null, null, - queries.uniqueDomains(), queries.forwarded(), queries.cached(), null, null, queries.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(), null, - blockingAnswer.blocking(), null); - return Optional.of(translated); + if (statAnswer != null && blockingAnswer != null) { + Queries queries = statAnswer.queries(); + Replies replies = queries.replies(); + DnsStatistics translated = new DnsStatistics(statAnswer.gravity().domainsBeingBlocked(), null, null, + null, queries.uniqueDomains(), queries.forwarded(), queries.cached(), null, null, + queries.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(), null, blockingAnswer.blocking(), null); + return Optional.of(translated); + } + } + return Optional.empty(); } @Override @@ -105,10 +110,12 @@ public void enableBlocking() throws PiHoleException { } private void internalBlock(Blocking action) throws PiHoleException { - var url = apiUrl.resolve(apiUrl.getPath() + "/dns/blocking"); - var request = client.newRequest(url).timeout(TIMEOUT_SECONDS, SECONDS).method(HttpMethod.POST) - .header(HttpHeader.ACCEPT, "application/json").header("sid", session.sid()) - .content(new StringContentProvider(gson.toJson(action))); - send(request); + if (session instanceof Session local) { + var url = apiUrl.resolve(apiUrl.getPath() + "/dns/blocking"); + var request = client.newRequest(url).timeout(TIMEOUT_SECONDS, SECONDS).method(HttpMethod.POST) + .header(HttpHeader.ACCEPT, "application/json").header("sid", local.sid()) + .content(new StringContentProvider(gson.toJson(action))); + send(request); + } } } diff --git a/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/model/v6/DnsBlockingAnswer.java b/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/model/v6/DnsBlockingAnswer.java index de1b1077b0618..7260cd8ae7a51 100644 --- a/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/model/v6/DnsBlockingAnswer.java +++ b/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/model/v6/DnsBlockingAnswer.java @@ -5,7 +5,7 @@ * information. * * This program and the accompanying materials are made available under the - * terms of the Eclipse License 2.0 which is available at + * 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 diff --git a/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/model/v6/StatAnswer.java b/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/model/v6/StatAnswer.java index f97fe6a6bf024..26c9c8c26c96e 100644 --- a/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/model/v6/StatAnswer.java +++ b/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/model/v6/StatAnswer.java @@ -5,7 +5,7 @@ * information. * * This program and the accompanying materials are made available under the - * terms of the Eclipse License 2.0 which is available at + * 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 From 2b5f5d82ffd9bffbf76daf6bd36e027caa264cc1 Mon Sep 17 00:00:00 2001 From: "gael@lhopital.org" Date: Fri, 19 Sep 2025 17:07:59 +0200 Subject: [PATCH 4/5] Finalizing API v6 for translated values of the handler. Code review corrections. Signed-off-by: gael@lhopital.org --- bundles/org.openhab.binding.pihole/README.md | 4 +- .../pihole/internal/PiHoleHandlerFactory.java | 7 +- .../pihole/internal/rest/AdminService.java | 8 +- .../internal/rest/JettyAdminService.java | 4 +- .../internal/rest/JettyAdminServiceV6.java | 166 ++++++++++++------ .../internal/rest/model/v6/ConfigAnswer.java | 132 ++++++++++++++ .../internal/rest/model/v6/HistoryAnswer.java | 25 +++ .../rest/model/v6/HistoryClients.java | 32 ++++ .../internal/rest/model/v6/SessionAnswer.java | 4 +- .../internal/rest/model/v6/StatAnswer.java | 2 +- .../rest/model/v6/StatDatabaseSummary.java | 23 +++ .../resources/OH-INF/thing/thing-types.xml | 3 +- 12 files changed, 345 insertions(+), 65 deletions(-) create mode 100644 bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/model/v6/ConfigAnswer.java create mode 100644 bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/model/v6/HistoryAnswer.java create mode 100644 bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/model/v6/HistoryClients.java create mode 100644 bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/model/v6/StatDatabaseSummary.java diff --git a/bundles/org.openhab.binding.pihole/README.md b/bundles/org.openhab.binding.pihole/README.md index f21840aabb3da..51585fe6cf33d 100644 --- a/bundles/org.openhab.binding.pihole/README.md +++ b/bundles/org.openhab.binding.pihole/README.md @@ -22,10 +22,10 @@ The Pi-hole Binding allows you to monitor Pi-hole statistics and control its fun | Name | Type | Description | Default | Required | Advanced | |-----------------|---------|-------------------------------------------------------------------------------------------|---------|----------|----------| -| hostname | text | Url (hostname or IP address) of the pihole web server | 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 | | 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 | yes | +| serverVersion | text | Defines the API to be used with this server | v5 | no | no | ## Channels diff --git a/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/PiHoleHandlerFactory.java b/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/PiHoleHandlerFactory.java index 617e841dade8f..1b4d8f581d902 100644 --- a/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/PiHoleHandlerFactory.java +++ b/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/PiHoleHandlerFactory.java @@ -65,8 +65,9 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) { protected @Nullable ThingHandler createHandler(Thing thing) { ThingTypeUID thingTypeUID = thing.getThingTypeUID(); - return PI_HOLE_TYPE.equals(thingTypeUID) - ? new PiHoleHandler(thing, timeZoneProvider, httpClientFactory.getCommonHttpClient(), GSON) - : null; + if (PI_HOLE_TYPE.equals(thingTypeUID)) { + return new PiHoleHandler(thing, timeZoneProvider, httpClientFactory.getCommonHttpClient(), GSON); + } + return null; } } diff --git a/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/AdminService.java b/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/AdminService.java index 1bdc06564efab..c6cba9b589a1b 100644 --- a/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/AdminService.java +++ b/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/AdminService.java @@ -33,14 +33,10 @@ public abstract class AdminService { protected static final long TIMEOUT_SECONDS = 10L; - protected final String token; - protected final HttpClient client; protected final Gson gson; - public AdminService(String token, HttpClient client, Gson gson) { - super(); - this.token = token; + public AdminService(HttpClient client, Gson gson) { this.client = client; this.gson = gson; } @@ -68,7 +64,7 @@ public AdminService(String token, HttpClient client, Gson gson) { */ public abstract void enableBlocking() throws PiHoleException; - protected static ContentResponse send(Request request) throws PiHoleException { + protected ContentResponse send(Request request) throws PiHoleException { try { return request.send(); } catch (InterruptedException | TimeoutException | ExecutionException e) { diff --git a/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/JettyAdminService.java b/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/JettyAdminService.java index a52db509286a8..0cc4bdb00a6dc 100644 --- a/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/JettyAdminService.java +++ b/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/JettyAdminService.java @@ -34,10 +34,12 @@ public class JettyAdminService extends AdminService { private final Logger logger = LoggerFactory.getLogger(JettyAdminService.class); private final URI baseUrl; + private final String token; public JettyAdminService(String token, URI baseUrl, HttpClient client, Gson gson) { - super(token, client, gson); + super(client, gson); this.baseUrl = baseUrl; + this.token = token; } @Override diff --git a/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/JettyAdminServiceV6.java b/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/JettyAdminServiceV6.java index 0481a85daad7d..09fedafdcaa99 100644 --- a/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/JettyAdminServiceV6.java +++ b/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/JettyAdminServiceV6.java @@ -15,8 +15,12 @@ import static java.util.concurrent.TimeUnit.SECONDS; import java.net.URI; +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; @@ -25,14 +29,19 @@ 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.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; @@ -44,78 +53,137 @@ @NonNullByDefault public class JettyAdminServiceV6 extends AdminService { private final Logger logger = LoggerFactory.getLogger(JettyAdminServiceV6.class); - protected final URI apiUrl; - private @Nullable Session session; + 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(token, client, gson); - apiUrl = baseUrl.resolve("/api"); + 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 void getAuth() throws PiHoleException { - logger.debug("Check if authentication is required"); - var authUrl = apiUrl.resolve(apiUrl.getPath() + "/auth"); - var request = client.newRequest(authUrl).timeout(TIMEOUT_SECONDS, SECONDS).method(HttpMethod.POST) - .header(HttpHeader.ACCEPT, "application/json") - .content(new StringContentProvider(gson.toJson(new Password(token)))); - var response = send(request); - var content = response.getContentAsString(); + 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 = answer.session(); + 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 summary() throws PiHoleException { logger.debug("Getting summary"); - getAuth(); - if (session instanceof Session local) { - var url = apiUrl.resolve(apiUrl.getPath() + "/stats/summary"); - var request = client.newRequest(url).timeout(TIMEOUT_SECONDS, SECONDS) - .header(HttpHeader.ACCEPT, "application/json").header("sid", local.sid()); - var response = send(request); - StatAnswer statAnswer = gson.fromJson(response.getContentAsString(), StatAnswer.class); - - url = apiUrl.resolve(apiUrl.getPath() + "/dns/blocking"); - request = client.newRequest(url).timeout(TIMEOUT_SECONDS, SECONDS) - .header(HttpHeader.ACCEPT, "application/json").header("sid", local.sid()); - response = send(request); - DnsBlockingAnswer blockingAnswer = gson.fromJson(response.getContentAsString(), DnsBlockingAnswer.class); - - if (statAnswer != null && blockingAnswer != null) { - Queries queries = statAnswer.queries(); - Replies replies = queries.replies(); - DnsStatistics translated = new DnsStatistics(statAnswer.gravity().domainsBeingBlocked(), null, null, - null, queries.uniqueDomains(), queries.forwarded(), queries.cached(), null, null, - queries.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(), null, blockingAnswer.blocking(), null); - return Optional.of(translated); - } - } - return Optional.empty(); + StatAnswer statAnswer = get(statsSummaryURI, StatAnswer.class); + Queries statQueries = statAnswer.queries(); + Replies replies = statQueries.replies(); + Gravity gravity = statAnswer.gravity(); + + 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); + + 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(), null)); + + return Optional.of(translated); } @Override public void disableBlocking(long seconds) throws PiHoleException { logger.debug("Disabling blocking for {} seconds", seconds); - internalBlock(new Blocking(false, seconds)); + post(dnsBlockingURI, new Blocking(false, seconds)); } @Override public void enableBlocking() throws PiHoleException { logger.debug("Enabling blocking"); - internalBlock(Blocking.BLOCK); + post(dnsBlockingURI, Blocking.BLOCK); } - private void internalBlock(Blocking action) throws PiHoleException { - if (session instanceof Session local) { - var url = apiUrl.resolve(apiUrl.getPath() + "/dns/blocking"); - var request = client.newRequest(url).timeout(TIMEOUT_SECONDS, SECONDS).method(HttpMethod.POST) - .header(HttpHeader.ACCEPT, "application/json").header("sid", local.sid()) - .content(new StringContentProvider(gson.toJson(action))); - send(request); + 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 get(URI targetURI, Class 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)); } } diff --git a/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/model/v6/ConfigAnswer.java b/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/model/v6/ConfigAnswer.java new file mode 100644 index 0000000000000..48bf383a2bf62 --- /dev/null +++ b/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/model/v6/ConfigAnswer.java @@ -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 excludeClients, List 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 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 upstreams, boolean cNAMEdeepInspect, boolean blockESNI, boolean edns0ecs, + boolean ignoreLocalhost, boolean showDNSSEC, boolean analyzeOnlyAandAAAA, String piholePTR, + String replyWhenBusy, int blockTTL, List hosts, boolean domainNeeded, boolean expandHosts, + String domain, boolean bogusPriv, boolean dnssec, String _interface, String hostRecord, + String listeningMode, boolean queryLogging, List cnameRecords, int port, List 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 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 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) { + } +} diff --git a/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/model/v6/HistoryAnswer.java b/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/model/v6/HistoryAnswer.java new file mode 100644 index 0000000000000..e6200b74a442e --- /dev/null +++ b/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/model/v6/HistoryAnswer.java @@ -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) { + + } +} diff --git a/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/model/v6/HistoryClients.java b/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/model/v6/HistoryClients.java new file mode 100644 index 0000000000000..804c45d8eb937 --- /dev/null +++ b/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/model/v6/HistoryClients.java @@ -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 clients, List history, double took) { + public record Client(String name, int total) { + + } + + public record History(double timestamp, Map data) { + + } +} diff --git a/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/model/v6/SessionAnswer.java b/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/model/v6/SessionAnswer.java index 750ad34f99dfb..b57697d66c548 100644 --- a/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/model/v6/SessionAnswer.java +++ b/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/model/v6/SessionAnswer.java @@ -20,6 +20,8 @@ @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; + } } } diff --git a/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/model/v6/StatAnswer.java b/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/model/v6/StatAnswer.java index 26c9c8c26c96e..fa2440f78e68d 100644 --- a/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/model/v6/StatAnswer.java +++ b/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/model/v6/StatAnswer.java @@ -25,7 +25,7 @@ record Clients(int active, int total) { } - public record Gravity(int domainsBeingBlocked, int lastUpdate) { + public record Gravity(int domainsBeingBlocked, long lastUpdate) { } diff --git a/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/model/v6/StatDatabaseSummary.java b/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/model/v6/StatDatabaseSummary.java new file mode 100644 index 0000000000000..e4f8916313d36 --- /dev/null +++ b/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/model/v6/StatDatabaseSummary.java @@ -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) { +} diff --git a/bundles/org.openhab.binding.pihole/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.pihole/src/main/resources/OH-INF/thing/thing-types.xml index ddf7145b1605b..9cc3c89e5d64d 100644 --- a/bundles/org.openhab.binding.pihole/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.pihole/src/main/resources/OH-INF/thing/thing-types.xml @@ -124,7 +124,7 @@ network-address - Url (hostname or IP address) of the pihole web server + URL (hostname or IP address) of the pihole web server password @@ -141,7 +141,6 @@ Defines the API to be used with this server. v5 - true From 09987e9247871adf97e0d4aea4fb35ab5f29d7ac Mon Sep 17 00:00:00 2001 From: "gael@lhopital.org" Date: Fri, 19 Sep 2025 17:30:59 +0200 Subject: [PATCH 5/5] Adding missing Relative field. Signed-off-by: gael@lhopital.org --- .../pihole/internal/rest/JettyAdminServiceV6.java | 12 ++++++++---- .../pihole/internal/rest/model/v6/StatAnswer.java | 6 +++++- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/JettyAdminServiceV6.java b/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/JettyAdminServiceV6.java index 09fedafdcaa99..668e29b45e083 100644 --- a/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/JettyAdminServiceV6.java +++ b/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/JettyAdminServiceV6.java @@ -15,6 +15,7 @@ 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; @@ -30,6 +31,7 @@ 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; @@ -118,11 +120,11 @@ private long getTodayMidnight() { @Override public Optional summary() throws PiHoleException { - logger.debug("Getting summary"); + 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(); - Gravity gravity = statAnswer.gravity(); DnsBlockingAnswer blockingAnswer = get(dnsBlockingURI, DnsBlockingAnswer.class); @@ -132,6 +134,8 @@ public Optional summary() throws PiHoleException { 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(), @@ -139,8 +143,8 @@ public Optional summary() throws PiHoleException { 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(), null)); + configAnswer.config().misc().privacylevel(), blockingAnswer.blocking(), new GravityLastUpdated( + configAnswer.config().files().gravity() != null, gravity.lastUpdate(), relative)); return Optional.of(translated); } diff --git a/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/model/v6/StatAnswer.java b/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/model/v6/StatAnswer.java index fa2440f78e68d..5b8c4c9f9db14 100644 --- a/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/model/v6/StatAnswer.java +++ b/bundles/org.openhab.binding.pihole/src/main/java/org/openhab/binding/pihole/internal/rest/model/v6/StatAnswer.java @@ -12,6 +12,8 @@ */ 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; @@ -26,7 +28,9 @@ 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,