]> git.basschouten.com Git - openhab-addons.git/blob
71dd547672682ae2bf752dcee64d677559f7be35
[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.hydrawise.internal.handler;
14
15 import java.io.IOException;
16 import java.util.ArrayList;
17 import java.util.Collection;
18 import java.util.Collections;
19 import java.util.List;
20 import java.util.concurrent.ScheduledFuture;
21 import java.util.concurrent.TimeUnit;
22
23 import org.eclipse.jdt.annotation.NonNullByDefault;
24 import org.eclipse.jdt.annotation.Nullable;
25 import org.eclipse.jetty.client.HttpClient;
26 import org.openhab.binding.hydrawise.internal.HydrawiseControllerListener;
27 import org.openhab.binding.hydrawise.internal.api.HydrawiseAuthenticationException;
28 import org.openhab.binding.hydrawise.internal.api.HydrawiseConnectionException;
29 import org.openhab.binding.hydrawise.internal.api.graphql.HydrawiseGraphQLClient;
30 import org.openhab.binding.hydrawise.internal.api.graphql.dto.Customer;
31 import org.openhab.binding.hydrawise.internal.api.graphql.dto.QueryResponse;
32 import org.openhab.binding.hydrawise.internal.config.HydrawiseAccountConfiguration;
33 import org.openhab.binding.hydrawise.internal.discovery.HydrawiseCloudControllerDiscoveryService;
34 import org.openhab.core.auth.client.oauth2.AccessTokenRefreshListener;
35 import org.openhab.core.auth.client.oauth2.AccessTokenResponse;
36 import org.openhab.core.auth.client.oauth2.OAuthClientService;
37 import org.openhab.core.auth.client.oauth2.OAuthException;
38 import org.openhab.core.auth.client.oauth2.OAuthFactory;
39 import org.openhab.core.auth.client.oauth2.OAuthResponseException;
40 import org.openhab.core.config.core.Configuration;
41 import org.openhab.core.thing.Bridge;
42 import org.openhab.core.thing.ChannelUID;
43 import org.openhab.core.thing.ThingStatus;
44 import org.openhab.core.thing.ThingStatusDetail;
45 import org.openhab.core.thing.binding.BaseBridgeHandler;
46 import org.openhab.core.thing.binding.ThingHandlerService;
47 import org.openhab.core.types.Command;
48 import org.slf4j.Logger;
49 import org.slf4j.LoggerFactory;
50
51 /**
52  * The {@link HydrawiseAccountHandler} is responsible for handling for connecting to a Hydrawise account and polling for
53  * controller data
54  *
55  * @author Dan Cunningham - Initial contribution
56  */
57 @NonNullByDefault
58 public class HydrawiseAccountHandler extends BaseBridgeHandler implements AccessTokenRefreshListener {
59     private final Logger logger = LoggerFactory.getLogger(HydrawiseAccountHandler.class);
60     /**
61      * Minimum amount of time we can poll for updates
62      */
63     private static final int MIN_REFRESH_SECONDS = 30;
64     private static final String BASE_URL = "https://app.hydrawise.com/api/v2/";
65     private static final String AUTH_URL = BASE_URL + "oauth/access-token";
66     private static final String CLIENT_SECRET = "zn3CrjglwNV1";
67     private static final String CLIENT_ID = "hydrawise_app";
68     private static final String SCOPE = "all";
69     private final List<HydrawiseControllerListener> controllerListeners = Collections
70             .synchronizedList(new ArrayList<HydrawiseControllerListener>());
71     private final HttpClient httpClient;
72     private final OAuthFactory oAuthFactory;
73     private @Nullable OAuthClientService oAuthService;
74     private @Nullable HydrawiseGraphQLClient apiClient;
75     private @Nullable ScheduledFuture<?> pollFuture;
76     private @Nullable Customer lastData;
77     private int refresh;
78
79     public HydrawiseAccountHandler(final Bridge bridge, final HttpClient httpClient, final OAuthFactory oAuthFactory) {
80         super(bridge);
81         this.httpClient = httpClient;
82         this.oAuthFactory = oAuthFactory;
83     }
84
85     @Override
86     public void handleCommand(ChannelUID channelUID, Command command) {
87     }
88
89     @Override
90     public void initialize() {
91         OAuthClientService oAuthService = oAuthFactory.createOAuthClientService(getThing().toString(), AUTH_URL,
92                 AUTH_URL, CLIENT_ID, CLIENT_SECRET, SCOPE, false);
93         this.oAuthService = oAuthService;
94         oAuthService.addAccessTokenRefreshListener(this);
95         this.apiClient = new HydrawiseGraphQLClient(httpClient, oAuthService);
96         logger.debug("Handler initialized.");
97         scheduler.schedule(() -> configure(oAuthService), 0, TimeUnit.SECONDS);
98     }
99
100     @Override
101     public void dispose() {
102         logger.debug("Handler disposed.");
103         clearPolling();
104         OAuthClientService oAuthService = this.oAuthService;
105         if (oAuthService != null) {
106             oAuthService.removeAccessTokenRefreshListener(this);
107             oAuthFactory.ungetOAuthService(getThing().toString());
108             this.oAuthService = null;
109         }
110     }
111
112     @Override
113     public void handleRemoval() {
114         OAuthClientService oAuthService = this.oAuthService;
115         if (oAuthService != null) {
116             oAuthFactory.deleteServiceAndAccessToken(getThing().toString());
117         }
118         super.handleRemoval();
119     }
120
121     @Override
122     public void onAccessTokenResponse(AccessTokenResponse tokenResponse) {
123         logger.debug("Auth Token Refreshed, expires in {}", tokenResponse.getExpiresIn());
124     }
125
126     @Override
127     public Collection<Class<? extends ThingHandlerService>> getServices() {
128         return Collections.singleton(HydrawiseCloudControllerDiscoveryService.class);
129     }
130
131     public void addControllerListeners(HydrawiseControllerListener listener) {
132         this.controllerListeners.add(listener);
133         Customer data = lastData;
134         if (data != null) {
135             listener.onData(data.controllers);
136         }
137     }
138
139     public void removeControllerListeners(HydrawiseControllerListener listener) {
140         synchronized (controllerListeners) {
141             this.controllerListeners.remove(listener);
142         }
143     }
144
145     public @Nullable HydrawiseGraphQLClient graphQLClient() {
146         return apiClient;
147     }
148
149     public @Nullable Customer lastData() {
150         return lastData;
151     }
152
153     public void refreshData(int delaySeconds) {
154         initPolling(delaySeconds, this.refresh);
155     }
156
157     private void configure(OAuthClientService oAuthService) {
158         HydrawiseAccountConfiguration config = getConfig().as(HydrawiseAccountConfiguration.class);
159         try {
160             if (!config.userName.isEmpty() && !config.password.isEmpty()) {
161                 if (!config.savePassword) {
162                     Configuration editedConfig = editConfiguration();
163                     editedConfig.remove("password");
164                     updateConfiguration(editedConfig);
165                 }
166                 oAuthService.getAccessTokenByResourceOwnerPasswordCredentials(config.userName, config.password, SCOPE);
167             } else if (oAuthService.getAccessTokenResponse() == null) {
168                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Login credentials required.");
169                 return;
170             }
171             this.refresh = Math.max(config.refreshInterval, MIN_REFRESH_SECONDS);
172             initPolling(0, refresh);
173         } catch (OAuthException | IOException e) {
174             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
175         } catch (OAuthResponseException e) {
176             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Login credentials required.");
177         }
178     }
179
180     /**
181      * Starts/Restarts polling with an initial delay. This allows changes in the poll cycle for when commands are sent
182      * and we need to poll sooner then the next refresh cycle.
183      */
184     private synchronized void initPolling(int initalDelay, int refresh) {
185         clearPolling();
186         pollFuture = scheduler.scheduleWithFixedDelay(this::poll, initalDelay, refresh, TimeUnit.SECONDS);
187     }
188
189     /**
190      * Stops/clears this thing's polling future
191      */
192     private void clearPolling() {
193         ScheduledFuture<?> localFuture = pollFuture;
194         if (isFutureValid(localFuture)) {
195             if (localFuture != null) {
196                 localFuture.cancel(false);
197             }
198         }
199     }
200
201     private boolean isFutureValid(@Nullable ScheduledFuture<?> future) {
202         return future != null && !future.isCancelled();
203     }
204
205     private void poll() {
206         poll(true);
207     }
208
209     private void poll(boolean retry) {
210         try {
211             QueryResponse response = apiClient.queryControllers();
212             if (response == null) {
213                 throw new HydrawiseConnectionException("Malformed response");
214             }
215             if (response.errors != null && response.errors.size() > 0) {
216                 throw new HydrawiseConnectionException(response.errors.stream().map(error -> error.message).reduce("",
217                         (messages, message) -> messages + message + ". "));
218             }
219             if (getThing().getStatus() != ThingStatus.ONLINE) {
220                 updateStatus(ThingStatus.ONLINE);
221             }
222             lastData = response.data.me;
223             synchronized (controllerListeners) {
224                 controllerListeners.forEach(listener -> {
225                     listener.onData(response.data.me.controllers);
226                 });
227             }
228         } catch (HydrawiseConnectionException e) {
229             if (retry) {
230                 logger.debug("Retrying failed poll", e);
231                 poll(false);
232             } else {
233                 logger.debug("Will try again during next poll period", e);
234                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
235             }
236         } catch (HydrawiseAuthenticationException e) {
237             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
238             clearPolling();
239         }
240     }
241 }