]> git.basschouten.com Git - openhab-addons.git/blob
ed7e10bd2fec9090dfe3385398506c9a36ce9cb3
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 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.ambientweather.internal.handler;
14
15 import static org.openhab.binding.ambientweather.internal.AmbientWeatherBindingConstants.CONFIG_MAC_ADDRESS;
16
17 import java.io.IOException;
18 import java.util.concurrent.ScheduledFuture;
19 import java.util.concurrent.TimeUnit;
20
21 import org.apache.commons.lang.StringUtils;
22 import org.eclipse.jdt.annotation.NonNullByDefault;
23 import org.eclipse.jdt.annotation.Nullable;
24 import org.openhab.binding.ambientweather.internal.config.BridgeConfig;
25 import org.openhab.binding.ambientweather.internal.model.DeviceJson;
26 import org.openhab.core.io.net.http.HttpUtil;
27 import org.openhab.core.thing.Bridge;
28 import org.openhab.core.thing.ChannelUID;
29 import org.openhab.core.thing.Thing;
30 import org.openhab.core.thing.ThingStatus;
31 import org.openhab.core.thing.ThingStatusDetail;
32 import org.openhab.core.thing.binding.BaseBridgeHandler;
33 import org.openhab.core.thing.binding.ThingHandler;
34 import org.openhab.core.types.Command;
35 import org.slf4j.Logger;
36 import org.slf4j.LoggerFactory;
37
38 import com.google.gson.Gson;
39 import com.google.gson.JsonSyntaxException;
40
41 /**
42  * The {@link AmbientWeatherBridgeHandler} is responsible for handling the
43  * bridge things created to use the Ambient Weather service.
44  *
45  * @author Mark Hilbush - Initial Contribution
46  */
47 @NonNullByDefault
48 public class AmbientWeatherBridgeHandler extends BaseBridgeHandler {
49     // URL to retrieve device list from Ambient Weather
50     private static final String DEVICES_URL = "https://api.ambientweather.net/v1/devices?applicationKey=%APPKEY%&apiKey=%APIKEY%";
51
52     // Timeout of the call to the Ambient Weather devices API
53     public static final int DEVICES_API_TIMEOUT = 20000;
54
55     // Time to wait after failed key validation
56     public static final long KEY_VALIDATION_DELAY = 60L;
57
58     private final Logger logger = LoggerFactory.getLogger(AmbientWeatherBridgeHandler.class);
59
60     // Job to validate app and api keys
61     @Nullable
62     private ScheduledFuture<?> validateKeysJob;
63
64     // Application key is granted only by request from developer
65     private String applicationKey = "";
66
67     // API key assigned to user in ambientweather.net dashboard
68     private String apiKey = "";
69
70     // Used Ambient Weather real-time API to retrieve weather data
71     // for weather stations assigned to an API key
72     private AmbientWeatherEventListener listener;
73
74     private final Gson gson = new Gson();
75
76     private Runnable validateKeysRunnable = new Runnable() {
77         @Override
78         public void run() {
79             logger.debug("Validating application and API keys");
80
81             String response = null;
82             try {
83                 // Query weather stations (devices) from Ambient Weather
84                 String url = DEVICES_URL.replace("%APPKEY%", getApplicationKey()).replace("%APIKEY%", getApiKey());
85                 logger.debug("Bridge: Querying list of devices from ambient weather service");
86                 response = HttpUtil.executeUrl("GET", url, DEVICES_API_TIMEOUT);
87                 logger.trace("Bridge: Response = {}", response);
88                 // Got a response so the keys are good
89                 DeviceJson[] stations = gson.fromJson(response, DeviceJson[].class);
90                 logger.debug("Bridge: Application and API keys are valid with {} stations", stations.length);
91                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, "Connecting to service");
92                 // Start up the real-time API listener
93                 listener.start(applicationKey, apiKey, gson);
94             } catch (IOException e) {
95                 // executeUrl throws IOException when it gets a Not Authorized (401) response
96                 logger.debug("Bridge: Got IOException: {}", e.getMessage());
97                 setThingOfflineWithCommError(e.getMessage(), "Invalid API or application key");
98                 rescheduleValidateKeysJob();
99             } catch (IllegalArgumentException e) {
100                 logger.debug("Bridge: Got IllegalArgumentException: {}", e.getMessage());
101                 setThingOfflineWithCommError(e.getMessage(), "Unable to get devices");
102                 rescheduleValidateKeysJob();
103             } catch (JsonSyntaxException e) {
104                 logger.debug("Bridge: Got JsonSyntaxException: {}", e.getMessage());
105                 setThingOfflineWithCommError(e.getMessage(), "Error parsing json response");
106                 rescheduleValidateKeysJob();
107             }
108         }
109     };
110
111     public AmbientWeatherBridgeHandler(Bridge bridge) {
112         super(bridge);
113         listener = new AmbientWeatherEventListener(this);
114     }
115
116     @Override
117     public void initialize() {
118         updateStatus(ThingStatus.UNKNOWN);
119         // If there are keys in the config, schedule the job to validate them
120         if (hasApplicationKey() && hasApiKey()) {
121             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Awaiting key validation");
122             scheduleValidateKeysJob();
123         }
124     }
125
126     /*
127      * Check if an application key has been provided in the thing config
128      */
129     private boolean hasApplicationKey() {
130         String configApplicationKey = getConfigAs(BridgeConfig.class).applicationKey;
131         if (StringUtils.isEmpty(configApplicationKey)) {
132             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Missing application key");
133             return false;
134         }
135         applicationKey = configApplicationKey;
136         return true;
137     }
138
139     /*
140      * Check if an API key has been provided in the thing config
141      */
142     private boolean hasApiKey() {
143         String configApiKey = getConfigAs(BridgeConfig.class).apiKey;
144         if (StringUtils.isEmpty(configApiKey)) {
145             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Missing API key");
146             return false;
147         }
148         apiKey = configApiKey;
149         return true;
150     }
151
152     public void setThingOfflineWithCommError(@Nullable String errorDetail, @Nullable String statusDescription) {
153         String status = statusDescription != null ? statusDescription : "null";
154         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, status);
155     }
156
157     @Override
158     public void dispose() {
159         cancelValidateKeysJob();
160         listener.stop();
161     }
162
163     /**
164      * Start the job to validate the API and Application keys.
165      * A side-effect of this is that we will discover the devices
166      * MAC addresses that are associated with the API key.
167      */
168     private void scheduleValidateKeysJob() {
169         if (validateKeysJob == null) {
170             validateKeysJob = scheduler.schedule(validateKeysRunnable, 5, TimeUnit.SECONDS);
171         }
172     }
173
174     private void cancelValidateKeysJob() {
175         if (validateKeysJob != null) {
176             validateKeysJob.cancel(true);
177             validateKeysJob = null;
178         }
179     }
180
181     public void rescheduleValidateKeysJob() {
182         logger.debug("Bridge: Key validation will run again in {} seconds", KEY_VALIDATION_DELAY);
183         validateKeysJob = scheduler.schedule(validateKeysRunnable, KEY_VALIDATION_DELAY, TimeUnit.SECONDS);
184     }
185
186     /*
187      * Keep track of the station handlers so that the listener can route data events
188      * to the correct handler.
189      */
190     @Override
191     public void childHandlerInitialized(ThingHandler childHandler, Thing childThing) {
192         String macAddress = (String) childThing.getConfiguration().get(CONFIG_MAC_ADDRESS);
193         listener.addHandler((AmbientWeatherStationHandler) childHandler, macAddress);
194         logger.debug("Bridge: Station handler initialized for {} with MAC {}", childThing.getUID(), macAddress);
195         listener.resubscribe();
196     }
197
198     @Override
199     public void childHandlerDisposed(ThingHandler childHandler, Thing childThing) {
200         String macAddress = (String) childThing.getConfiguration().get(CONFIG_MAC_ADDRESS);
201         listener.removeHandler((AmbientWeatherStationHandler) childHandler, macAddress);
202         logger.debug("Bridge: Station handler disposed for {} with MAC {}", childThing.getUID(), macAddress);
203     }
204
205     // Callback used by EventListener to update bridge status
206     public void markBridgeOffline(String reason) {
207         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, reason);
208     }
209
210     // Callback used by EventListener to update bridge status
211     public void markBridgeOnline() {
212         updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE);
213     }
214
215     public String getApplicationKey() {
216         return applicationKey;
217     }
218
219     public String getApiKey() {
220         return apiKey;
221     }
222
223     @Override
224     public void handleCommand(ChannelUID channelUID, Command command) {
225         // Handler doesn't support any commands
226     }
227 }