2 * Copyright (c) 2010-2020 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.ambientweather.internal.handler;
15 import static org.openhab.binding.ambientweather.internal.AmbientWeatherBindingConstants.CONFIG_MAC_ADDRESS;
17 import java.io.IOException;
18 import java.util.concurrent.ScheduledFuture;
19 import java.util.concurrent.TimeUnit;
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;
38 import com.google.gson.Gson;
39 import com.google.gson.JsonSyntaxException;
42 * The {@link AmbientWeatherBridgeHandler} is responsible for handling the
43 * bridge things created to use the Ambient Weather service.
45 * @author Mark Hilbush - Initial Contribution
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%";
52 // Timeout of the call to the Ambient Weather devices API
53 public static final int DEVICES_API_TIMEOUT = 20000;
55 // Time to wait after failed key validation
56 public static final long KEY_VALIDATION_DELAY = 60L;
58 private final Logger logger = LoggerFactory.getLogger(AmbientWeatherBridgeHandler.class);
60 // Job to validate app and api keys
62 private ScheduledFuture<?> validateKeysJob;
64 // Application key is granted only by request from developer
66 private String applicationKey;
68 // API key assigned to user in ambientweather.net dashboard
70 private String apiKey;
72 // Used Ambient Weather real-time API to retrieve weather data
73 // for weather stations assigned to an API key
74 private AmbientWeatherEventListener listener;
76 private final Gson gson = new Gson();
78 private Runnable validateKeysRunnable = new Runnable() {
81 logger.debug("Validating application and API keys");
83 String response = null;
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();
113 public AmbientWeatherBridgeHandler(Bridge bridge) {
115 listener = new AmbientWeatherEventListener(this);
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();
129 * Check if an application key has been provided in the thing config
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");
137 applicationKey = configApplicationKey;
142 * Check if an API key has been provided in the thing config
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");
150 apiKey = configApiKey;
154 public void setThingOfflineWithCommError(String errorDetail, String statusDescription) {
155 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, statusDescription);
159 public void dispose() {
160 cancelValidateKeysJob();
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.
169 private void scheduleValidateKeysJob() {
170 if (validateKeysJob == null) {
171 validateKeysJob = scheduler.schedule(validateKeysRunnable, 5, TimeUnit.SECONDS);
175 private void cancelValidateKeysJob() {
176 if (validateKeysJob != null) {
177 validateKeysJob.cancel(true);
178 validateKeysJob = null;
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);
188 * Keep track of the station handlers so that the listener can route data events
189 * to the correct handler.
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();
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);
206 // Callback used by EventListener to update bridge status
207 public void markBridgeOffline(String reason) {
208 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, reason);
211 // Callback used by EventListener to update bridge status
212 public void markBridgeOnline() {
213 updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE);
216 public @Nullable String getApplicationKey() {
217 return applicationKey;
220 public @Nullable String getApiKey() {
225 public void handleCommand(ChannelUID channelUID, Command command) {
226 // Handler doesn't support any commands