2 * Copyright (c) 2010-2023 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.coronastats.internal.handler;
15 import java.net.SocketTimeoutException;
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;
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;
48 import com.google.gson.Gson;
49 import com.google.gson.JsonSyntaxException;
52 * The {@link CoronaStatsWorldHandler} is the handler for bridge thing
54 * @author Johannes Ott - Initial contribution
57 public class CoronaStatsWorldHandler extends BaseBridgeHandler {
58 private static final String CORONASTATS_URL = "https://corona-stats.online/?format=json";
60 private final Logger logger = LoggerFactory.getLogger(CoronaStatsWorldHandler.class);
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();
69 public CoronaStatsWorldHandler(Bridge bridge, HttpClient client) {
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);
85 public void initialize() {
86 logger.debug("Initializing Corona Stats bridge handler");
87 worldConfig = getConfigAs(CoronaStatsWorldConfiguration.class);
89 if (worldConfig.isValid()) {
91 updateStatus(ThingStatus.UNKNOWN);
93 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
94 "Refresh interval has to be at least 15 minutes.");
99 public void dispose() {
100 logger.debug("Handler disposed.");
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);
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);
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);
128 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
129 pollException.getMessage());
132 updateStatus(ThingStatus.ONLINE);
133 notifyOnUpdate(resultCoronaStats);
140 private CompletableFuture<@Nullable CoronaStats> requestRefresh() {
141 CompletableFuture<@Nullable CoronaStats> f = new CompletableFuture<>();
142 Request request = client.newRequest(URI.create(CORONASTATS_URL));
144 request.method(HttpMethod.GET).timeout(2000, TimeUnit.SECONDS).send(new BufferingResponseListener() {
145 @NonNullByDefault({})
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));
154 f.completeExceptionally(new CoronaStatsPollingException("Request failed", e));
156 } else if (response.getStatus() != 200) {
157 f.completeExceptionally(new CoronaStatsPollingException(getContentAsString()));
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"));
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);
184 logger.warn("Tried to add listener {} but it was already present. This is probably an error.",
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.",
201 public void notifyOnUpdate(@Nullable CoronaStats newCoronaStats) {
202 if (newCoronaStats != null) {
203 coronaStats = newCoronaStats;
205 CoronaStatsWorld world = newCoronaStats.getWorld();
207 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "World stats not found");
211 world.getChannelsStateMap().forEach(this::updateState);
212 countryListeners.forEach(listener -> listener.notifyOnUpdate(newCoronaStats));
216 public @Nullable CoronaStats getCoronaStats() {