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