]> git.basschouten.com Git - openhab-addons.git/blob
1b54e258068f49dcf6db9bdbb71deefb833bfffb
[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.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 listener) {
176             logger.debug("Register thing listener.");
177             if (countryListeners.add(listener)) {
178                 final CoronaStats localCoronaStats = coronaStats;
179                 if (localCoronaStats != null) {
180                     listener.notifyOnUpdate(localCoronaStats);
181                 }
182             } else {
183                 logger.warn("Tried to add listener {} but it was already present. This is probably an error.",
184                         childHandler);
185             }
186         }
187     }
188
189     @Override
190     public void childHandlerDisposed(ThingHandler childHandler, Thing childThing) {
191         if (childHandler instanceof CoronaStatsCountryHandler countryHandler) {
192             logger.debug("Unregister thing listener.");
193             if (!countryListeners.remove(countryHandler)) {
194                 logger.warn("Tried to remove listener {} but it was not registered. This is probably an error.",
195                         childHandler);
196             }
197         }
198     }
199
200     public void notifyOnUpdate(@Nullable CoronaStats newCoronaStats) {
201         if (newCoronaStats != null) {
202             coronaStats = newCoronaStats;
203
204             CoronaStatsWorld world = newCoronaStats.getWorld();
205             if (world == null) {
206                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "World stats not found");
207                 return;
208             }
209
210             world.getChannelsStateMap().forEach(this::updateState);
211             countryListeners.forEach(listener -> listener.notifyOnUpdate(newCoronaStats));
212         }
213     }
214
215     public @Nullable CoronaStats getCoronaStats() {
216         return coronaStats;
217     }
218 }