]> git.basschouten.com Git - openhab-addons.git/blob
d373a9c2e64e97e3cc5913f3fc272bdc6bcef000
[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     @Nullable
66     private String applicationKey;
67
68     // API key assigned to user in ambientweather.net dashboard
69     @Nullable
70     private String apiKey;
71
72     // Used Ambient Weather real-time API to retrieve weather data
73     // for weather stations assigned to an API key
74     private AmbientWeatherEventListener listener;
75
76     private final Gson gson = new Gson();
77
78     private Runnable validateKeysRunnable = new Runnable() {
79         @Override
80         public void run() {
81             logger.debug("Validating application and API keys");
82
83             String response = null;
84             try {
85                 // Query weather stations (devices) from Ambient Weather
86                 String url = DEVICES_URL.replace("%APPKEY%", getApplicationKey()).replace("%APIKEY%", getApiKey());
87                 logger.debug("Bridge: Querying list of devices from ambient weather service");
88                 response = HttpUtil.executeUrl("GET", url, DEVICES_API_TIMEOUT);
89                 logger.trace("Bridge: Response = {}", response);
90                 // Got a response so the keys are good
91                 DeviceJson[] stations = gson.fromJson(response, DeviceJson[].class);
92                 logger.debug("Bridge: Application and API keys are valid with {} stations", stations.length);
93                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, "Connecting to service");
94                 // Start up the real-time API listener
95                 listener.start(applicationKey, apiKey, gson);
96             } catch (IOException e) {
97                 // executeUrl throws IOException when it gets a Not Authorized (401) response
98                 logger.debug("Bridge: Got IOException: {}", e.getMessage());
99                 setThingOfflineWithCommError(e.getMessage(), "Invalid API or application key");
100                 rescheduleValidateKeysJob();
101             } catch (IllegalArgumentException e) {
102                 logger.debug("Bridge: Got IllegalArgumentException: {}", e.getMessage());
103                 setThingOfflineWithCommError(e.getMessage(), "Unable to get devices");
104                 rescheduleValidateKeysJob();
105             } catch (JsonSyntaxException e) {
106                 logger.debug("Bridge: Got JsonSyntaxException: {}", e.getMessage());
107                 setThingOfflineWithCommError(e.getMessage(), "Error parsing json response");
108                 rescheduleValidateKeysJob();
109             }
110         }
111     };
112
113     public AmbientWeatherBridgeHandler(Bridge bridge) {
114         super(bridge);
115         listener = new AmbientWeatherEventListener(this);
116     }
117
118     @Override
119     public void initialize() {
120         updateStatus(ThingStatus.UNKNOWN);
121         // If there are keys in the config, schedule the job to validate them
122         if (hasApplicationKey() && hasApiKey()) {
123             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Awaiting key validation");
124             scheduleValidateKeysJob();
125         }
126     }
127
128     /*
129      * Check if an application key has been provided in the thing config
130      */
131     private boolean hasApplicationKey() {
132         String configApplicationKey = getConfigAs(BridgeConfig.class).applicationKey;
133         if (StringUtils.isEmpty(configApplicationKey)) {
134             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Missing application key");
135             return false;
136         }
137         applicationKey = configApplicationKey;
138         return true;
139     }
140
141     /*
142      * Check if an API key has been provided in the thing config
143      */
144     private boolean hasApiKey() {
145         String configApiKey = getConfigAs(BridgeConfig.class).apiKey;
146         if (StringUtils.isEmpty(configApiKey)) {
147             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Missing API key");
148             return false;
149         }
150         apiKey = configApiKey;
151         return true;
152     }
153
154     public void setThingOfflineWithCommError(String errorDetail, String statusDescription) {
155         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, statusDescription);
156     }
157
158     @Override
159     public void dispose() {
160         cancelValidateKeysJob();
161         listener.stop();
162     }
163
164     /**
165      * Start the job to validate the API and Application keys.
166      * A side-effect of this is that we will discover the devices
167      * MAC addresses that are associated with the API key.
168      */
169     private void scheduleValidateKeysJob() {
170         if (validateKeysJob == null) {
171             validateKeysJob = scheduler.schedule(validateKeysRunnable, 5, TimeUnit.SECONDS);
172         }
173     }
174
175     private void cancelValidateKeysJob() {
176         if (validateKeysJob != null) {
177             validateKeysJob.cancel(true);
178             validateKeysJob = null;
179         }
180     }
181
182     public void rescheduleValidateKeysJob() {
183         logger.debug("Bridge: Key validation will run again in {} seconds", KEY_VALIDATION_DELAY);
184         validateKeysJob = scheduler.schedule(validateKeysRunnable, KEY_VALIDATION_DELAY, TimeUnit.SECONDS);
185     }
186
187     /*
188      * Keep track of the station handlers so that the listener can route data events
189      * to the correct handler.
190      */
191     @Override
192     public void childHandlerInitialized(ThingHandler childHandler, Thing childThing) {
193         String macAddress = (String) childThing.getConfiguration().get(CONFIG_MAC_ADDRESS);
194         listener.addHandler((AmbientWeatherStationHandler) childHandler, macAddress);
195         logger.debug("Bridge: Station handler initialized for {} with MAC {}", childThing.getUID(), macAddress);
196         listener.resubscribe();
197     }
198
199     @Override
200     public void childHandlerDisposed(ThingHandler childHandler, Thing childThing) {
201         String macAddress = (String) childThing.getConfiguration().get(CONFIG_MAC_ADDRESS);
202         listener.removeHandler((AmbientWeatherStationHandler) childHandler, macAddress);
203         logger.debug("Bridge: Station handler disposed for {} with MAC {}", childThing.getUID(), macAddress);
204     }
205
206     // Callback used by EventListener to update bridge status
207     public void markBridgeOffline(String reason) {
208         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, reason);
209     }
210
211     // Callback used by EventListener to update bridge status
212     public void markBridgeOnline() {
213         updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE);
214     }
215
216     public @Nullable String getApplicationKey() {
217         return applicationKey;
218     }
219
220     public @Nullable String getApiKey() {
221         return apiKey;
222     }
223
224     @Override
225     public void handleCommand(ChannelUID channelUID, Command command) {
226         // Handler doesn't support any commands
227     }
228 }