]> git.basschouten.com Git - openhab-addons.git/blob
8a00513cdb5f1fd737c61a89fe35a43450251f4c
[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.openuv.internal.handler;
14
15 import java.io.IOException;
16 import java.math.BigDecimal;
17 import java.time.Duration;
18 import java.time.LocalDate;
19 import java.time.LocalDateTime;
20 import java.util.Collection;
21 import java.util.Optional;
22 import java.util.Properties;
23 import java.util.Set;
24 import java.util.concurrent.ScheduledFuture;
25 import java.util.concurrent.TimeUnit;
26
27 import org.eclipse.jdt.annotation.NonNullByDefault;
28 import org.eclipse.jdt.annotation.Nullable;
29 import org.openhab.binding.openuv.internal.OpenUVException;
30 import org.openhab.binding.openuv.internal.config.BridgeConfiguration;
31 import org.openhab.binding.openuv.internal.discovery.OpenUVDiscoveryService;
32 import org.openhab.binding.openuv.internal.json.OpenUVResponse;
33 import org.openhab.binding.openuv.internal.json.OpenUVResult;
34 import org.openhab.core.i18n.LocaleProvider;
35 import org.openhab.core.i18n.LocationProvider;
36 import org.openhab.core.i18n.TranslationProvider;
37 import org.openhab.core.io.net.http.HttpUtil;
38 import org.openhab.core.library.types.PointType;
39 import org.openhab.core.thing.Bridge;
40 import org.openhab.core.thing.ChannelUID;
41 import org.openhab.core.thing.ThingStatus;
42 import org.openhab.core.thing.ThingStatusDetail;
43 import org.openhab.core.thing.binding.BaseBridgeHandler;
44 import org.openhab.core.thing.binding.ThingHandlerService;
45 import org.openhab.core.types.Command;
46 import org.openhab.core.types.RefreshType;
47 import org.slf4j.Logger;
48 import org.slf4j.LoggerFactory;
49
50 import com.google.gson.Gson;
51 import com.google.gson.JsonSyntaxException;
52
53 /**
54  * {@link OpenUVBridgeHandler} is the handler for OpenUV API and connects it
55  * to the webservice.
56  *
57  * @author GaĆ«l L'hopital - Initial contribution
58  *
59  */
60 @NonNullByDefault
61 public class OpenUVBridgeHandler extends BaseBridgeHandler {
62     private static final String QUERY_URL = "https://api.openuv.io/api/v1/uv?lat=%.2f&lng=%.2f&alt=%.0f";
63     private static final int RECONNECT_DELAY_MIN = 5;
64     private static final int REQUEST_TIMEOUT_MS = (int) TimeUnit.SECONDS.toMillis(30);
65
66     private final Logger logger = LoggerFactory.getLogger(OpenUVBridgeHandler.class);
67     private final Properties header = new Properties();
68     private final Gson gson;
69     private final LocationProvider locationProvider;
70     private final TranslationProvider i18nProvider;
71     private final LocaleProvider localeProvider;
72
73     private Optional<ScheduledFuture<?>> reconnectJob = Optional.empty();
74     private boolean keyVerified;
75
76     public OpenUVBridgeHandler(Bridge bridge, LocationProvider locationProvider, TranslationProvider i18nProvider,
77             LocaleProvider localeProvider, Gson gson) {
78         super(bridge);
79         this.gson = gson;
80         this.locationProvider = locationProvider;
81         this.i18nProvider = i18nProvider;
82         this.localeProvider = localeProvider;
83     }
84
85     @Override
86     public void initialize() {
87         logger.debug("Initializing OpenUV API bridge handler.");
88         keyVerified = false;
89         BridgeConfiguration config = getConfigAs(BridgeConfiguration.class);
90         if (config.apikey.isEmpty()) {
91             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
92                     "@text/offline.config-error-unknown-apikey");
93             return;
94         }
95         header.put("x-access-token", config.apikey);
96         initiateConnexion();
97     }
98
99     @Override
100     public void dispose() {
101         header.clear();
102         freeReconnectJob();
103     }
104
105     @Override
106     public void handleCommand(ChannelUID channelUID, Command command) {
107         if (command instanceof RefreshType) {
108             initiateConnexion();
109         } else {
110             logger.debug("The OpenUV bridge only handles Refresh command and not '{}'", command);
111         }
112     }
113
114     private void initiateConnexion() {
115         // Just checking if the provided api key is a valid one by making a fake call
116         getUVData(BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO);
117     }
118
119     public @Nullable OpenUVResult getUVData(BigDecimal latitude, BigDecimal longitude, BigDecimal altitude) {
120         String statusMessage = "";
121         ThingStatusDetail statusDetail = ThingStatusDetail.COMMUNICATION_ERROR;
122         String url = QUERY_URL.formatted(latitude, longitude, altitude);
123         String jsonData = "";
124         try {
125             jsonData = HttpUtil.executeUrl("GET", url, header, null, null, REQUEST_TIMEOUT_MS);
126             OpenUVResponse uvResponse = gson.fromJson(jsonData, OpenUVResponse.class);
127             if (uvResponse != null) {
128                 String error = uvResponse.getError();
129                 if (error == null) {
130                     updateStatus(ThingStatus.ONLINE);
131                     keyVerified = true;
132                     return uvResponse.getResult();
133                 }
134                 throw new OpenUVException(error);
135             }
136         } catch (JsonSyntaxException e) {
137             if (jsonData.contains("MongoError")) {
138                 statusMessage = "@text/offline.comm-error-faultly-service [ \"%d\" ]".formatted(RECONNECT_DELAY_MIN);
139                 scheduleReconnectJob(RECONNECT_DELAY_MIN);
140             } else {
141                 statusDetail = ThingStatusDetail.NONE;
142                 statusMessage = "@text/offline.invalid-json [ \"%s\" ]".formatted(url);
143                 logger.debug("{} : {}", statusMessage, jsonData);
144             }
145         } catch (IOException e) {
146             statusMessage = "@text/offline.comm-error-ioexception [ \"%s\",\"%d\" ]".formatted(e.getMessage(),
147                     RECONNECT_DELAY_MIN);
148             scheduleReconnectJob(RECONNECT_DELAY_MIN);
149         } catch (OpenUVException e) {
150             if (e.isQuotaError()) {
151                 LocalDateTime nextMidnight = LocalDate.now().plusDays(1).atStartOfDay().plusMinutes(2);
152                 statusMessage = "@text/offline.comm-error-quota-exceeded [ \"%s\" ]".formatted(nextMidnight.toString());
153                 scheduleReconnectJob(Duration.between(LocalDateTime.now(), nextMidnight).toMinutes());
154             } else if (e.isApiKeyError()) {
155                 if (keyVerified) {
156                     statusMessage = "@text/offline.api-key-not-recognized [ \"%d\" ]".formatted(RECONNECT_DELAY_MIN);
157                     scheduleReconnectJob(RECONNECT_DELAY_MIN);
158                 } else {
159                     statusDetail = ThingStatusDetail.CONFIGURATION_ERROR;
160                 }
161             }
162         }
163         updateStatus(ThingStatus.OFFLINE, statusDetail, statusMessage);
164         return null;
165     }
166
167     private void scheduleReconnectJob(long delay) {
168         freeReconnectJob();
169         reconnectJob = Optional.of(scheduler.schedule(this::initiateConnexion, delay, TimeUnit.MINUTES));
170     }
171
172     private void freeReconnectJob() {
173         reconnectJob.ifPresent(job -> job.cancel(true));
174         reconnectJob = Optional.empty();
175     }
176
177     @Override
178     public Collection<Class<? extends ThingHandlerService>> getServices() {
179         return Set.of(OpenUVDiscoveryService.class);
180     }
181
182     public @Nullable PointType getLocation() {
183         return locationProvider.getLocation();
184     }
185
186     public TranslationProvider getI18nProvider() {
187         return i18nProvider;
188     }
189
190     public LocaleProvider getLocaleProvider() {
191         return localeProvider;
192     }
193 }