2 * Copyright (c) 2010-2024 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.pihole.internal;
15 import static java.util.concurrent.TimeUnit.*;
16 import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.ADS_BLOCKED_TODAY_CHANNEL;
17 import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.ADS_PERCENTAGE_TODAY_CHANNEL;
18 import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.CLIENTS_EVER_SEEN_CHANNEL;
19 import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.DISABLE_ENABLE_CHANNEL;
20 import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.DNS_QUERIES_ALL_REPLIES_CHANNEL;
21 import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.DNS_QUERIES_ALL_TYPES_CHANNEL;
22 import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.DNS_QUERIES_TODAY_CHANNEL;
23 import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.DOMAINS_BEING_BLOCKED_CHANNEL;
24 import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.DisableEnable;
25 import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.DisableEnable.ENABLE;
26 import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.ENABLED_CHANNEL;
27 import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.PRIVACY_LEVEL_CHANNEL;
28 import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.QUERIES_CACHED_CHANNEL;
29 import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.QUERIES_FORWARDED_CHANNEL;
30 import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.REPLY_BLOB_CHANNEL;
31 import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.REPLY_CNAME_CHANNEL;
32 import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.REPLY_DNSSEC_CHANNEL;
33 import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.REPLY_DOMAIN_CHANNEL;
34 import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.REPLY_IP_CHANNEL;
35 import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.REPLY_NODATA_CHANNEL;
36 import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.REPLY_NONE_CHANNEL;
37 import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.REPLY_NOTIMP_CHANNEL;
38 import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.REPLY_NXDOMAIN_CHANNEL;
39 import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.REPLY_OTHER_CHANNEL;
40 import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.REPLY_REFUSED_CHANNEL;
41 import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.REPLY_RRNAME_CHANNEL;
42 import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.REPLY_SERVFAIL_CHANNEL;
43 import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.REPLY_UNKNOWN_CHANNEL;
44 import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.UNIQUE_CLIENTS_CHANNEL;
45 import static org.openhab.binding.pihole.internal.PiHoleBindingConstants.Channels.UNIQUE_DOMAINS_CHANNEL;
46 import static org.openhab.core.library.unit.Units.PERCENT;
47 import static org.openhab.core.thing.ThingStatus.OFFLINE;
48 import static org.openhab.core.thing.ThingStatus.ONLINE;
49 import static org.openhab.core.thing.ThingStatus.UNKNOWN;
50 import static org.openhab.core.thing.ThingStatusDetail.*;
52 import java.math.BigDecimal;
54 import java.net.URISyntaxException;
55 import java.util.Collection;
56 import java.util.Optional;
58 import java.util.concurrent.ScheduledFuture;
60 import org.eclipse.jdt.annotation.NonNullByDefault;
61 import org.eclipse.jdt.annotation.Nullable;
62 import org.eclipse.jetty.client.HttpClient;
63 import org.openhab.binding.pihole.internal.rest.AdminService;
64 import org.openhab.binding.pihole.internal.rest.JettyAdminService;
65 import org.openhab.binding.pihole.internal.rest.model.DnsStatistics;
66 import org.openhab.core.library.types.DecimalType;
67 import org.openhab.core.library.types.OnOffType;
68 import org.openhab.core.library.types.QuantityType;
69 import org.openhab.core.library.types.StringType;
70 import org.openhab.core.thing.ChannelUID;
71 import org.openhab.core.thing.Thing;
72 import org.openhab.core.thing.binding.BaseThingHandler;
73 import org.openhab.core.thing.binding.ThingHandlerService;
74 import org.openhab.core.types.Command;
75 import org.openhab.core.types.RefreshType;
76 import org.slf4j.Logger;
77 import org.slf4j.LoggerFactory;
80 * The {@link PiHoleHandler} is responsible for handling commands, which are
81 * sent to one of the channels.
83 * @author Martin Grzeslowski - Initial contribution
86 public class PiHoleHandler extends BaseThingHandler implements AdminService {
87 private static final int HTTP_DELAY_SECONDS = 1;
88 private final Logger logger = LoggerFactory.getLogger(PiHoleHandler.class);
89 private final Object lock = new Object();
90 private final HttpClient httpClient;
92 private @Nullable AdminService adminService;
93 private @Nullable DnsStatistics dnsStatistics;
94 private @Nullable ScheduledFuture<?> scheduledFuture;
96 public PiHoleHandler(Thing thing, HttpClient httpClient) {
98 this.httpClient = httpClient;
102 public void initialize() {
103 // set the thing status to UNKNOWN temporarily and let the background task decide for the real status.
104 // the framework is then able to reuse the resources from the thing handler initialization.
105 // we set this upfront to reliably check status updates in unit tests.
106 updateStatus(UNKNOWN);
108 var config = getConfigAs(PiHoleConfiguration.class);
110 if (config.refreshIntervalSeconds <= 0) {
111 updateStatus(OFFLINE, CONFIGURATION_ERROR, "@text/handler.init.wrongInterval");
117 hostname = new URI(config.hostname);
118 } catch (URISyntaxException e) {
119 updateStatus(OFFLINE, CONFIGURATION_ERROR,
120 "@token/handler.init.invalidHostname[\"" + config.hostname + "\"]");
123 if (config.token.isEmpty()) {
124 updateStatus(OFFLINE, CONFIGURATION_ERROR, "@token/handler.init.noToken");
127 adminService = new JettyAdminService(config.token, hostname, httpClient);
128 scheduledFuture = scheduler.scheduleWithFixedDelay(this::update, 0, config.refreshIntervalSeconds, SECONDS);
130 // do not set status here, the background task will do it.
133 private void update() {
134 var local = adminService;
139 // this block can be called from at least 2 threads
140 // check disableBlocking method
141 synchronized (lock) {
143 logger.debug("Refreshing DnsStatistics from Pi-hole");
144 local.summary().ifPresent(statistics -> dnsStatistics = statistics);
146 updateStatus(ONLINE);
147 } catch (Exception e) {
148 logger.debug("Error occurred when refreshing DnsStatistics from Pi-hole", e);
149 updateStatus(OFFLINE, COMMUNICATION_ERROR, e.getLocalizedMessage());
155 public void handleCommand(ChannelUID channelUID, Command command) {
156 if (command instanceof RefreshType) {
161 if (DISABLE_ENABLE_CHANNEL.equals(channelUID.getId())) {
162 if (command instanceof StringType stringType) {
163 var value = DisableEnable.valueOf(stringType.toString());
166 case DISABLE -> disableBlocking(0);
167 case FOR_10_SEC -> disableBlocking(10);
168 case FOR_30_SEC -> disableBlocking(30);
169 case FOR_5_MIN -> disableBlocking(MINUTES.toSeconds(5));
170 case ENABLE -> enableBlocking();
172 } catch (PiHoleException ex) {
173 logger.debug("Cannot invoke {} on channel {}", value, channelUID, ex);
174 updateStatus(OFFLINE, COMMUNICATION_ERROR, ex.getLocalizedMessage());
180 private void refresh() {
181 var localDnsStatistics = dnsStatistics;
182 if (localDnsStatistics == null) {
186 updateDecimalState(DOMAINS_BEING_BLOCKED_CHANNEL, localDnsStatistics.domainsBeingBlocked());
187 updateDecimalState(DNS_QUERIES_TODAY_CHANNEL, localDnsStatistics.dnsQueriesToday());
188 updateDecimalState(ADS_BLOCKED_TODAY_CHANNEL, localDnsStatistics.adsBlockedToday());
189 updateDecimalState(UNIQUE_DOMAINS_CHANNEL, localDnsStatistics.uniqueDomains());
190 updateDecimalState(QUERIES_FORWARDED_CHANNEL, localDnsStatistics.queriesForwarded());
191 updateDecimalState(QUERIES_CACHED_CHANNEL, localDnsStatistics.queriesCached());
192 updateDecimalState(CLIENTS_EVER_SEEN_CHANNEL, localDnsStatistics.clientsEverSeen());
193 updateDecimalState(UNIQUE_CLIENTS_CHANNEL, localDnsStatistics.uniqueClients());
194 updateDecimalState(DNS_QUERIES_ALL_TYPES_CHANNEL, localDnsStatistics.dnsQueriesAllTypes());
195 updateDecimalState(REPLY_UNKNOWN_CHANNEL, localDnsStatistics.replyUnknown());
196 updateDecimalState(REPLY_NODATA_CHANNEL, localDnsStatistics.replyNoData());
197 updateDecimalState(REPLY_NXDOMAIN_CHANNEL, localDnsStatistics.replyNXDomain());
198 updateDecimalState(REPLY_CNAME_CHANNEL, localDnsStatistics.replyCName());
199 updateDecimalState(REPLY_IP_CHANNEL, localDnsStatistics.replyIP());
200 updateDecimalState(REPLY_DOMAIN_CHANNEL, localDnsStatistics.replyDomain());
201 updateDecimalState(REPLY_RRNAME_CHANNEL, localDnsStatistics.replyRRName());
202 updateDecimalState(REPLY_SERVFAIL_CHANNEL, localDnsStatistics.replyServFail());
203 updateDecimalState(REPLY_REFUSED_CHANNEL, localDnsStatistics.replyRefused());
204 updateDecimalState(REPLY_NOTIMP_CHANNEL, localDnsStatistics.replyNotImp());
205 updateDecimalState(REPLY_OTHER_CHANNEL, localDnsStatistics.replyOther());
206 updateDecimalState(REPLY_DNSSEC_CHANNEL, localDnsStatistics.replyDNSSEC());
207 updateDecimalState(REPLY_NONE_CHANNEL, localDnsStatistics.replyNone());
208 updateDecimalState(REPLY_BLOB_CHANNEL, localDnsStatistics.replyBlob());
209 updateDecimalState(DNS_QUERIES_ALL_REPLIES_CHANNEL, localDnsStatistics.dnsQueriesAllTypes());
210 updateDecimalState(PRIVACY_LEVEL_CHANNEL, localDnsStatistics.privacyLevel());
212 var adsPercentageToday = localDnsStatistics.adsPercentageToday();
213 if (adsPercentageToday != null) {
214 var state = new QuantityType<>(new BigDecimal(adsPercentageToday.toString()), PERCENT);
215 updateState(ADS_PERCENTAGE_TODAY_CHANNEL, state);
217 updateState(ENABLED_CHANNEL, OnOffType.from(localDnsStatistics.enabled()));
218 if (localDnsStatistics.enabled()) {
219 updateState(DISABLE_ENABLE_CHANNEL, new StringType(ENABLE.toString()));
223 private void updateDecimalState(String channelID, @Nullable Integer value) {
227 updateState(channelID, new DecimalType(value));
231 public Collection<Class<? extends ThingHandlerService>> getServices() {
232 return Set.of(PiHoleActions.class);
236 public void dispose() {
238 dnsStatistics = null;
239 var localScheduledFuture = scheduledFuture;
240 if (localScheduledFuture != null) {
241 localScheduledFuture.cancel(true);
242 scheduledFuture = null;
248 public Optional<DnsStatistics> summary() throws PiHoleException {
249 var local = adminService;
251 throw new IllegalStateException("AdminService not initialized");
253 return local.summary();
257 public void disableBlocking(long seconds) throws PiHoleException {
258 var local = adminService;
260 throw new IllegalStateException("AdminService not initialized");
262 local.disableBlocking(seconds);
263 // update the summary to get the value of DISABLED_CHANNEL channel
264 scheduler.schedule(this::update, HTTP_DELAY_SECONDS, SECONDS);
266 // update the summary to get the value of ENABLED_CHANNEL channel
267 // after the X seconds it probably will be true again
268 scheduler.schedule(this::update, seconds + HTTP_DELAY_SECONDS, SECONDS);
273 public void enableBlocking() throws PiHoleException {
274 var local = adminService;
276 throw new IllegalStateException("AdminService not initialized");
278 local.enableBlocking();
279 // update the summary to get the value of DISABLED_CHANNEL channel
280 scheduler.schedule(this::update, HTTP_DELAY_SECONDS, SECONDS);