]> git.basschouten.com Git - openhab-addons.git/blob
b84e4bd9231e291995223276ef1a69127864612d
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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.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;
36
37 import com.google.gson.Gson;
38 import com.google.gson.JsonSyntaxException;
39
40 /**
41  * The {@link AmbientWeatherBridgeHandler} is responsible for handling the
42  * bridge things created to use the Ambient Weather service.
43  *
44  * @author Mark Hilbush - Initial Contribution
45  */
46 @NonNullByDefault
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%";
50
51     // Timeout of the call to the Ambient Weather devices API
52     public static final int DEVICES_API_TIMEOUT = 20000;
53
54     // Time to wait after failed key validation
55     public static final long KEY_VALIDATION_DELAY = 60L;
56
57     private final Logger logger = LoggerFactory.getLogger(AmbientWeatherBridgeHandler.class);
58
59     // Job to validate app and api keys
60     @Nullable
61     private ScheduledFuture<?> validateKeysJob;
62
63     // Application key is granted only by request from developer
64     private String applicationKey = "";
65
66     // API key assigned to user in ambientweather.net dashboard
67     private String apiKey = "";
68
69     // Used Ambient Weather real-time API to retrieve weather data
70     // for weather stations assigned to an API key
71     private AmbientWeatherEventListener listener;
72
73     private final Gson gson = new Gson();
74
75     private Runnable validateKeysRunnable = new Runnable() {
76         @Override
77         public void run() {
78             logger.debug("Validating application and API keys");
79
80             String response = null;
81             try {
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();
106             }
107         }
108     };
109
110     public AmbientWeatherBridgeHandler(Bridge bridge) {
111         super(bridge);
112         listener = new AmbientWeatherEventListener(this);
113     }
114
115     @Override
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();
122         }
123     }
124
125     /*
126      * Check if an application key has been provided in the thing config
127      */
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");
132             return false;
133         }
134         applicationKey = configApplicationKey;
135         return true;
136     }
137
138     /*
139      * Check if an API key has been provided in the thing config
140      */
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");
145             return false;
146         }
147         apiKey = configApiKey;
148         return true;
149     }
150
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);
154     }
155
156     @Override
157     public void dispose() {
158         cancelValidateKeysJob();
159         listener.stop();
160     }
161
162     /**
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.
166      */
167     private void scheduleValidateKeysJob() {
168         if (validateKeysJob == null) {
169             validateKeysJob = scheduler.schedule(validateKeysRunnable, 5, TimeUnit.SECONDS);
170         }
171     }
172
173     private void cancelValidateKeysJob() {
174         if (validateKeysJob != null) {
175             validateKeysJob.cancel(true);
176             validateKeysJob = null;
177         }
178     }
179
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);
183     }
184
185     /*
186      * Keep track of the station handlers so that the listener can route data events
187      * to the correct handler.
188      */
189     @Override
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();
195     }
196
197     @Override
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);
202     }
203
204     // Callback used by EventListener to update bridge status
205     public void markBridgeOffline(String reason) {
206         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, reason);
207     }
208
209     // Callback used by EventListener to update bridge status
210     public void markBridgeOnline() {
211         updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE);
212     }
213
214     public String getApplicationKey() {
215         return applicationKey;
216     }
217
218     public String getApiKey() {
219         return apiKey;
220     }
221
222     @Override
223     public void handleCommand(ChannelUID channelUID, Command command) {
224         // Handler doesn't support any commands
225     }
226 }