]> git.basschouten.com Git - openhab-addons.git/blob
518675075483ae9cd3291b615f1daca0f02469ed
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2022 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.coronastats.internal.handler;
14
15 import java.net.SocketTimeoutException;
16 import java.net.URI;
17 import java.util.Set;
18 import java.util.concurrent.CompletableFuture;
19 import java.util.concurrent.ConcurrentHashMap;
20 import java.util.concurrent.ScheduledFuture;
21 import java.util.concurrent.TimeUnit;
22 import java.util.concurrent.TimeoutException;
23
24 import org.eclipse.jdt.annotation.NonNullByDefault;
25 import org.eclipse.jdt.annotation.Nullable;
26 import org.eclipse.jetty.client.HttpClient;
27 import org.eclipse.jetty.client.HttpResponse;
28 import org.eclipse.jetty.client.api.Request;
29 import org.eclipse.jetty.client.api.Result;
30 import org.eclipse.jetty.client.util.BufferingResponseListener;
31 import org.eclipse.jetty.http.HttpMethod;
32 import org.openhab.binding.coronastats.internal.CoronaStatsPollingException;
33 import org.openhab.binding.coronastats.internal.config.CoronaStatsWorldConfiguration;
34 import org.openhab.binding.coronastats.internal.dto.CoronaStats;
35 import org.openhab.binding.coronastats.internal.dto.CoronaStatsWorld;
36 import org.openhab.core.thing.Bridge;
37 import org.openhab.core.thing.ChannelUID;
38 import org.openhab.core.thing.Thing;
39 import org.openhab.core.thing.ThingStatus;
40 import org.openhab.core.thing.ThingStatusDetail;
41 import org.openhab.core.thing.binding.BaseBridgeHandler;
42 import org.openhab.core.thing.binding.ThingHandler;
43 import org.openhab.core.types.Command;
44 import org.openhab.core.types.RefreshType;
45 import org.slf4j.Logger;
46 import org.slf4j.LoggerFactory;
47
48 import com.google.gson.Gson;
49 import com.google.gson.JsonSyntaxException;
50
51 /**
52  * The {@link CoronaStatsWorldHandler} is the handler for bridge thing
53  *
54  * @author Johannes Ott - Initial contribution
55  */
56 @NonNullByDefault
57 public class CoronaStatsWorldHandler extends BaseBridgeHandler {
58     private static final String CORONASTATS_URL = "https://corona-stats.online/?format=json";
59
60     private final Logger logger = LoggerFactory.getLogger(CoronaStatsWorldHandler.class);
61
62     private CoronaStatsWorldConfiguration worldConfig = new CoronaStatsWorldConfiguration();
63     private @Nullable ScheduledFuture<?> pollingJob;
64     private @Nullable CoronaStats coronaStats;
65     private final Set<CoronaStatsCountryHandler> countryListeners = ConcurrentHashMap.newKeySet();
66     private final HttpClient client;
67     private final Gson gson = new Gson();
68
69     public CoronaStatsWorldHandler(Bridge bridge, HttpClient client) {
70         super(bridge);
71         this.client = client;
72     }
73
74     @Override
75     public void handleCommand(ChannelUID channelUID, Command command) {
76         if (command instanceof RefreshType) {
77             final CoronaStats localCoronaStats = coronaStats;
78             if (localCoronaStats != null) {
79                 notifyOnUpdate(localCoronaStats);
80             }
81         }
82     }
83
84     @Override
85     public void initialize() {
86         logger.debug("Initializing Corona Stats bridge handler");
87         worldConfig = getConfigAs(CoronaStatsWorldConfiguration.class);
88
89         if (worldConfig.isValid()) {
90             startPolling();
91             updateStatus(ThingStatus.UNKNOWN);
92         } else {
93             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
94                     "Refresh interval has to be at least 15 minutes.");
95         }
96     }
97
98     @Override
99     public void dispose() {
100         logger.debug("Handler disposed.");
101         stopPolling();
102     }
103
104     private void startPolling() {
105         final ScheduledFuture<?> localPollingJob = this.pollingJob;
106         if (localPollingJob == null || localPollingJob.isCancelled()) {
107             logger.debug("Start polling.");
108             pollingJob = scheduler.scheduleWithFixedDelay(this::poll, 0, worldConfig.refresh, TimeUnit.MINUTES);
109         }
110     }
111
112     private void stopPolling() {
113         final ScheduledFuture<?> localPollingJob = this.pollingJob;
114         if (localPollingJob != null && !localPollingJob.isCancelled()) {
115             logger.debug("Stop polling.");
116             localPollingJob.cancel(true);
117             pollingJob = null;
118         }
119     }
120
121     private void poll() {
122         logger.debug("Polling");
123         requestRefresh().handle((resultCoronaStats, pollException) -> {
124             if (resultCoronaStats == null) {
125                 if (pollException == null) {
126                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
127                 } else {
128                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
129                             pollException.getMessage());
130                 }
131             } else {
132                 updateStatus(ThingStatus.ONLINE);
133                 notifyOnUpdate(resultCoronaStats);
134             }
135
136             return null;
137         });
138     }
139
140     private CompletableFuture<@Nullable CoronaStats> requestRefresh() {
141         CompletableFuture<@Nullable CoronaStats> f = new CompletableFuture<>();
142         Request request = client.newRequest(URI.create(CORONASTATS_URL));
143
144         request.method(HttpMethod.GET).timeout(2000, TimeUnit.SECONDS).send(new BufferingResponseListener() {
145             @NonNullByDefault({})
146             @Override
147             public void onComplete(Result result) {
148                 final HttpResponse response = (HttpResponse) result.getResponse();
149                 if (result.getFailure() != null) {
150                     Throwable e = result.getFailure();
151                     if (e instanceof SocketTimeoutException || e instanceof TimeoutException) {
152                         f.completeExceptionally(new CoronaStatsPollingException("Request timeout", e));
153                     } else {
154                         f.completeExceptionally(new CoronaStatsPollingException("Request failed", e));
155                     }
156                 } else if (response.getStatus() != 200) {
157                     f.completeExceptionally(new CoronaStatsPollingException(getContentAsString()));
158                 } else {
159                     try {
160                         CoronaStats coronaStatsJSON = gson.fromJson(getContentAsString(), CoronaStats.class);
161                         f.complete(coronaStatsJSON);
162                     } catch (JsonSyntaxException parseException) {
163                         logger.error("Parsing failed: {}", parseException.getMessage());
164                         f.completeExceptionally(new CoronaStatsPollingException("Parsing of response failed"));
165                     }
166                 }
167             }
168         });
169
170         return f;
171     }
172
173     @Override
174     public void childHandlerInitialized(ThingHandler childHandler, Thing childThing) {
175         if (childHandler instanceof CoronaStatsCountryHandler) {
176             logger.debug("Register thing listener.");
177             final CoronaStatsCountryHandler listener = (CoronaStatsCountryHandler) childHandler;
178             if (countryListeners.add(listener)) {
179                 final CoronaStats localCoronaStats = coronaStats;
180                 if (localCoronaStats != null) {
181                     listener.notifyOnUpdate(localCoronaStats);
182                 }
183             } else {
184                 logger.warn("Tried to add listener {} but it was already present. This is probably an error.",
185                         childHandler);
186             }
187         }
188     }
189
190     @Override
191     public void childHandlerDisposed(ThingHandler childHandler, Thing childThing) {
192         if (childHandler instanceof CoronaStatsCountryHandler) {
193             logger.debug("Unregister thing listener.");
194             if (!countryListeners.remove((CoronaStatsCountryHandler) childHandler)) {
195                 logger.warn("Tried to remove listener {} but it was not registered. This is probably an error.",
196                         childHandler);
197             }
198         }
199     }
200
201     public void notifyOnUpdate(@Nullable CoronaStats newCoronaStats) {
202         if (newCoronaStats != null) {
203             coronaStats = newCoronaStats;
204
205             CoronaStatsWorld world = newCoronaStats.getWorld();
206             if (world == null) {
207                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "World stats not found");
208                 return;
209             }
210
211             world.getChannelsStateMap().forEach(this::updateState);
212             countryListeners.forEach(listener -> listener.notifyOnUpdate(newCoronaStats));
213         }
214     }
215
216     public @Nullable CoronaStats getCoronaStats() {
217         return coronaStats;
218     }
219 }