2 * Copyright (c) 2010-2021 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
65 private String applicationKey = "";
67 // API key assigned to user in ambientweather.net dashboard
68 private String apiKey = "";
70 // Used Ambient Weather real-time API to retrieve weather data
71 // for weather stations assigned to an API key
72 private AmbientWeatherEventListener listener;
74 private final Gson gson = new Gson();
76 private Runnable validateKeysRunnable = new Runnable() {
79 logger.debug("Validating application and API keys");
81 String response = null;
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();
111 public AmbientWeatherBridgeHandler(Bridge bridge) {
113 listener = new AmbientWeatherEventListener(this);
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();
127 * Check if an application key has been provided in the thing config
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");
135 applicationKey = configApplicationKey;
140 * Check if an API key has been provided in the thing config
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");
148 apiKey = configApiKey;
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);
158 public void dispose() {
159 cancelValidateKeysJob();
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.
168 private void scheduleValidateKeysJob() {
169 if (validateKeysJob == null) {
170 validateKeysJob = scheduler.schedule(validateKeysRunnable, 5, TimeUnit.SECONDS);
174 private void cancelValidateKeysJob() {
175 if (validateKeysJob != null) {
176 validateKeysJob.cancel(true);
177 validateKeysJob = null;
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);
187 * Keep track of the station handlers so that the listener can route data events
188 * to the correct handler.
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();
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);
205 // Callback used by EventListener to update bridge status
206 public void markBridgeOffline(String reason) {
207 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, reason);
210 // Callback used by EventListener to update bridge status
211 public void markBridgeOnline() {
212 updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE);
215 public String getApplicationKey() {
216 return applicationKey;
219 public String getApiKey() {
224 public void handleCommand(ChannelUID channelUID, Command command) {
225 // Handler doesn't support any commands