2 * Copyright (c) 2010-2023 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.eclipse.jdt.annotation.NonNullByDefault;
22 import org.eclipse.jdt.annotation.Nullable;
23 import org.openhab.binding.ambientweather.internal.config.BridgeConfig;
24 import org.openhab.binding.ambientweather.internal.model.DeviceJson;
25 import org.openhab.core.io.net.http.HttpUtil;
26 import org.openhab.core.thing.Bridge;
27 import org.openhab.core.thing.ChannelUID;
28 import org.openhab.core.thing.Thing;
29 import org.openhab.core.thing.ThingStatus;
30 import org.openhab.core.thing.ThingStatusDetail;
31 import org.openhab.core.thing.binding.BaseBridgeHandler;
32 import org.openhab.core.thing.binding.ThingHandler;
33 import org.openhab.core.types.Command;
34 import org.slf4j.Logger;
35 import org.slf4j.LoggerFactory;
37 import com.google.gson.Gson;
38 import com.google.gson.JsonSyntaxException;
41 * The {@link AmbientWeatherBridgeHandler} is responsible for handling the
42 * bridge things created to use the Ambient Weather service.
44 * @author Mark Hilbush - Initial Contribution
47 public class AmbientWeatherBridgeHandler extends BaseBridgeHandler {
48 // URL to retrieve device list from Ambient Weather
49 private static final String DEVICES_URL = "https://api.ambientweather.net/v1/devices?applicationKey=%APPKEY%&apiKey=%APIKEY%";
51 // Timeout of the call to the Ambient Weather devices API
52 public static final int DEVICES_API_TIMEOUT = 20000;
54 // Time to wait after failed key validation
55 public static final long KEY_VALIDATION_DELAY = 60L;
57 private final Logger logger = LoggerFactory.getLogger(AmbientWeatherBridgeHandler.class);
59 // Job to validate app and api keys
61 private ScheduledFuture<?> validateKeysJob;
63 // Application key is granted only by request from developer
64 private String applicationKey = "";
66 // API key assigned to user in ambientweather.net dashboard
67 private String apiKey = "";
69 // Used Ambient Weather real-time API to retrieve weather data
70 // for weather stations assigned to an API key
71 private AmbientWeatherEventListener listener;
73 private final Gson gson = new Gson();
75 private Runnable validateKeysRunnable = new Runnable() {
78 logger.debug("Validating application and API keys");
80 String response = null;
82 // Query weather stations (devices) from Ambient Weather
83 String url = DEVICES_URL.replace("%APPKEY%", getApplicationKey()).replace("%APIKEY%", getApiKey());
84 logger.debug("Bridge: Querying list of devices from ambient weather service");
85 response = HttpUtil.executeUrl("GET", url, DEVICES_API_TIMEOUT);
86 logger.trace("Bridge: Response = {}", response);
87 // Got a response so the keys are good
88 DeviceJson[] stations = gson.fromJson(response, DeviceJson[].class);
89 logger.debug("Bridge: Application and API keys are valid with {} stations", stations.length);
90 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, "Connecting to service");
91 // Start up the real-time API listener
92 listener.start(applicationKey, apiKey, gson);
93 } catch (IOException e) {
94 // executeUrl throws IOException when it gets a Not Authorized (401) response
95 logger.debug("Bridge: Got IOException: {}", e.getMessage());
96 setThingOfflineWithCommError(e.getMessage(), "Invalid API or application key");
97 rescheduleValidateKeysJob();
98 } catch (IllegalArgumentException e) {
99 logger.debug("Bridge: Got IllegalArgumentException: {}", e.getMessage());
100 setThingOfflineWithCommError(e.getMessage(), "Unable to get devices");
101 rescheduleValidateKeysJob();
102 } catch (JsonSyntaxException e) {
103 logger.debug("Bridge: Got JsonSyntaxException: {}", e.getMessage());
104 setThingOfflineWithCommError(e.getMessage(), "Error parsing json response");
105 rescheduleValidateKeysJob();
110 public AmbientWeatherBridgeHandler(Bridge bridge) {
112 listener = new AmbientWeatherEventListener(this);
116 public void initialize() {
117 updateStatus(ThingStatus.UNKNOWN);
118 // If there are keys in the config, schedule the job to validate them
119 if (hasApplicationKey() && hasApiKey()) {
120 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Awaiting key validation");
121 scheduleValidateKeysJob();
126 * Check if an application key has been provided in the thing config
128 private boolean hasApplicationKey() {
129 String configApplicationKey = getConfigAs(BridgeConfig.class).applicationKey;
130 if (configApplicationKey == null || configApplicationKey.isEmpty()) {
131 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Missing application key");
134 applicationKey = configApplicationKey;
139 * Check if an API key has been provided in the thing config
141 private boolean hasApiKey() {
142 String configApiKey = getConfigAs(BridgeConfig.class).apiKey;
143 if (configApiKey == null || configApiKey.isEmpty()) {
144 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Missing API key");
147 apiKey = configApiKey;
151 public void setThingOfflineWithCommError(@Nullable String errorDetail, @Nullable String statusDescription) {
152 String status = statusDescription != null ? statusDescription : "null";
153 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, status);
157 public void dispose() {
158 cancelValidateKeysJob();
163 * Start the job to validate the API and Application keys.
164 * A side-effect of this is that we will discover the devices
165 * MAC addresses that are associated with the API key.
167 private void scheduleValidateKeysJob() {
168 if (validateKeysJob == null) {
169 validateKeysJob = scheduler.schedule(validateKeysRunnable, 5, TimeUnit.SECONDS);
173 private void cancelValidateKeysJob() {
174 if (validateKeysJob != null) {
175 validateKeysJob.cancel(true);
176 validateKeysJob = null;
180 public void rescheduleValidateKeysJob() {
181 logger.debug("Bridge: Key validation will run again in {} seconds", KEY_VALIDATION_DELAY);
182 validateKeysJob = scheduler.schedule(validateKeysRunnable, KEY_VALIDATION_DELAY, TimeUnit.SECONDS);
186 * Keep track of the station handlers so that the listener can route data events
187 * to the correct handler.
190 public void childHandlerInitialized(ThingHandler childHandler, Thing childThing) {
191 String macAddress = (String) childThing.getConfiguration().get(CONFIG_MAC_ADDRESS);
192 listener.addHandler((AmbientWeatherStationHandler) childHandler, macAddress);
193 logger.debug("Bridge: Station handler initialized for {} with MAC {}", childThing.getUID(), macAddress);
194 listener.resubscribe();
198 public void childHandlerDisposed(ThingHandler childHandler, Thing childThing) {
199 String macAddress = (String) childThing.getConfiguration().get(CONFIG_MAC_ADDRESS);
200 listener.removeHandler((AmbientWeatherStationHandler) childHandler, macAddress);
201 logger.debug("Bridge: Station handler disposed for {} with MAC {}", childThing.getUID(), macAddress);
204 // Callback used by EventListener to update bridge status
205 public void markBridgeOffline(String reason) {
206 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, reason);
209 // Callback used by EventListener to update bridge status
210 public void markBridgeOnline() {
211 updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE);
214 public String getApplicationKey() {
215 return applicationKey;
218 public String getApiKey() {
223 public void handleCommand(ChannelUID channelUID, Command command) {
224 // Handler doesn't support any commands