]> git.basschouten.com Git - openhab-addons.git/blob
c9b4767b98d91bfc4b7db038a269eb5f2108248e
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2022 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.api.graphql;
14
15 import java.io.BufferedReader;
16 import java.io.IOException;
17 import java.io.InputStream;
18 import java.io.InputStreamReader;
19 import java.lang.reflect.Type;
20 import java.util.Optional;
21 import java.util.concurrent.ExecutionException;
22 import java.util.concurrent.TimeoutException;
23 import java.util.concurrent.atomic.AtomicInteger;
24 import java.util.stream.Collectors;
25
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.eclipse.jdt.annotation.Nullable;
28 import org.eclipse.jetty.client.HttpClient;
29 import org.eclipse.jetty.client.api.ContentResponse;
30 import org.eclipse.jetty.client.api.Response;
31 import org.eclipse.jetty.client.util.StringContentProvider;
32 import org.eclipse.jetty.http.HttpMethod;
33 import org.openhab.binding.hydrawise.internal.api.HydrawiseAuthenticationException;
34 import org.openhab.binding.hydrawise.internal.api.HydrawiseCommandException;
35 import org.openhab.binding.hydrawise.internal.api.HydrawiseConnectionException;
36 import org.openhab.binding.hydrawise.internal.api.graphql.dto.ControllerStatus;
37 import org.openhab.binding.hydrawise.internal.api.graphql.dto.Forecast;
38 import org.openhab.binding.hydrawise.internal.api.graphql.dto.Mutation;
39 import org.openhab.binding.hydrawise.internal.api.graphql.dto.MutationResponse;
40 import org.openhab.binding.hydrawise.internal.api.graphql.dto.MutationResponse.MutationResponseStatus;
41 import org.openhab.binding.hydrawise.internal.api.graphql.dto.MutationResponse.StatusCode;
42 import org.openhab.binding.hydrawise.internal.api.graphql.dto.QueryRequest;
43 import org.openhab.binding.hydrawise.internal.api.graphql.dto.QueryResponse;
44 import org.openhab.binding.hydrawise.internal.api.graphql.dto.ScheduledRuns;
45 import org.openhab.binding.hydrawise.internal.api.graphql.dto.Sensor;
46 import org.openhab.binding.hydrawise.internal.api.graphql.dto.Zone;
47 import org.openhab.binding.hydrawise.internal.api.graphql.dto.ZoneRun;
48 import org.openhab.core.auth.client.oauth2.AccessTokenResponse;
49 import org.openhab.core.auth.client.oauth2.OAuthClientService;
50 import org.openhab.core.auth.client.oauth2.OAuthException;
51 import org.openhab.core.auth.client.oauth2.OAuthResponseException;
52 import org.slf4j.Logger;
53 import org.slf4j.LoggerFactory;
54
55 import com.google.gson.FieldNamingPolicy;
56 import com.google.gson.Gson;
57 import com.google.gson.GsonBuilder;
58 import com.google.gson.JsonDeserializationContext;
59 import com.google.gson.JsonDeserializer;
60 import com.google.gson.JsonElement;
61 import com.google.gson.JsonParseException;
62
63 /**
64  *
65  * @author Dan Cunningham - Initial contribution
66  *
67  */
68 @NonNullByDefault
69 public class HydrawiseGraphQLClient {
70     private final Logger logger = LoggerFactory.getLogger(HydrawiseGraphQLClient.class);
71
72     private final Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
73             .registerTypeAdapter(Zone.class, new ResponseDeserializer<Zone>())
74             .registerTypeAdapter(ScheduledRuns.class, new ResponseDeserializer<ScheduledRuns>())
75             .registerTypeAdapter(ZoneRun.class, new ResponseDeserializer<ZoneRun>())
76             .registerTypeAdapter(Forecast.class, new ResponseDeserializer<Forecast>())
77             .registerTypeAdapter(Sensor.class, new ResponseDeserializer<Forecast>())
78             .registerTypeAdapter(ControllerStatus.class, new ResponseDeserializer<ControllerStatus>()).create();
79
80     private static final String GRAPH_URL = "https://app.hydrawise.com/api/v2/graph";
81     private static final String MUTATION_START_ZONE = "startZone(zoneId: %d) { status }";
82     private static final String MUTATION_START_ZONE_CUSTOM = "startZone(zoneId: %d, customRunDuration: %d) { status }";
83     private static final String MUTATION_START_ALL_ZONES = "startAllZones(controllerId: %d){ status }";
84     private static final String MUTATION_START_ALL_ZONES_CUSTOM = "startAllZones(controllerId: %d, markRunAsScheduled: false, customRunDuration: %d ){ status }";
85     private static final String MUTATION_STOP_ZONE = "stopZone(zoneId: %d) { status }";
86     private static final String MUTATION_STOP_ALL_ZONES = "stopAllZones(controllerId: %d){ status }";
87     private static final String MUTATION_SUSPEND_ZONE = "suspendZone(zoneId: %d, until: \"%s\"){ status }";
88     private static final String MUTATION_SUSPEND_ALL_ZONES = "suspendAllZones(controllerId: %d, until: \"%s\"){ status }";
89     private static final String MUTATION_RESUME_ZONE = "resumeZone(zoneId: %d){ status }";
90     private static final String MUTATION_RESUME_ALL_ZONES = "resumeAllZones(controllerId: %d){ status }";
91
92     private final HttpClient httpClient;
93     private final OAuthClientService oAuthService;
94     private String queryString = "";
95
96     public HydrawiseGraphQLClient(HttpClient httpClient, OAuthClientService oAuthService) {
97         this.httpClient = httpClient;
98         this.oAuthService = oAuthService;
99     }
100
101     /**
102      * Sends a GrapQL query for controller data
103      *
104      * @return QueryResponse
105      * @throws HydrawiseConnectionException
106      * @throws HydrawiseAuthenticationException
107      */
108     public @Nullable QueryResponse queryControllers()
109             throws HydrawiseConnectionException, HydrawiseAuthenticationException {
110         QueryRequest query;
111         try {
112             query = new QueryRequest(getQueryString());
113         } catch (IOException e) {
114             throw new HydrawiseConnectionException(e);
115         }
116         String queryJson = gson.toJson(query);
117         String response = sendGraphQLQuery(queryJson);
118         return gson.fromJson(response, QueryResponse.class);
119     }
120
121     /***
122      * Stops a given relay
123      *
124      * @param relayId
125      * @throws HydrawiseConnectionException
126      * @throws HydrawiseAuthenticationException
127      * @throws HydrawiseCommandException
128      */
129     public void stopRelay(int relayId)
130             throws HydrawiseConnectionException, HydrawiseAuthenticationException, HydrawiseCommandException {
131         sendGraphQLMutation(String.format(MUTATION_STOP_ZONE, relayId));
132     }
133
134     /**
135      * Stops all relays on a given controller
136      *
137      * @param controllerId
138      * @throws HydrawiseConnectionException
139      * @throws HydrawiseAuthenticationException
140      * @throws HydrawiseCommandException
141      */
142     public void stopAllRelays(int controllerId)
143             throws HydrawiseConnectionException, HydrawiseAuthenticationException, HydrawiseCommandException {
144         sendGraphQLMutation(String.format(MUTATION_STOP_ALL_ZONES, controllerId));
145     }
146
147     /**
148      * Runs a relay for the default amount of time
149      *
150      * @param relayId
151      * @throws HydrawiseConnectionException
152      * @throws HydrawiseAuthenticationException
153      * @throws HydrawiseCommandException
154      */
155     public void runRelay(int relayId)
156             throws HydrawiseConnectionException, HydrawiseAuthenticationException, HydrawiseCommandException {
157         sendGraphQLMutation(String.format(MUTATION_START_ZONE, relayId));
158     }
159
160     /**
161      * Runs a relay for the given amount of seconds
162      *
163      * @param relayId
164      * @param seconds
165      * @throws HydrawiseConnectionException
166      * @throws HydrawiseAuthenticationException
167      * @throws HydrawiseCommandException
168      */
169     public void runRelay(int relayId, int seconds)
170             throws HydrawiseConnectionException, HydrawiseAuthenticationException, HydrawiseCommandException {
171         sendGraphQLMutation(String.format(MUTATION_START_ZONE_CUSTOM, relayId, seconds));
172     }
173
174     /**
175      * Run all relays on a given controller for the default amount of time
176      *
177      * @param controllerId
178      * @throws HydrawiseConnectionException
179      * @throws HydrawiseAuthenticationException
180      * @throws HydrawiseCommandException
181      */
182     public void runAllRelays(int controllerId)
183             throws HydrawiseConnectionException, HydrawiseAuthenticationException, HydrawiseCommandException {
184         sendGraphQLMutation(String.format(MUTATION_START_ALL_ZONES, controllerId));
185     }
186
187     /***
188      * Run all relays on a given controller for the amount of seconds
189      *
190      * @param controllerId
191      * @param seconds
192      * @throws HydrawiseConnectionException
193      * @throws HydrawiseAuthenticationException
194      * @throws HydrawiseCommandException
195      */
196     public void runAllRelays(int controllerId, int seconds)
197             throws HydrawiseConnectionException, HydrawiseAuthenticationException, HydrawiseCommandException {
198         sendGraphQLMutation(String.format(MUTATION_START_ALL_ZONES_CUSTOM, controllerId, seconds));
199     }
200
201     /**
202      * Suspends a given relay
203      *
204      * @param relayId
205      * @throws HydrawiseConnectionException
206      * @throws HydrawiseAuthenticationException
207      * @throws HydrawiseCommandException
208      */
209     public void suspendRelay(int relayId, String until)
210             throws HydrawiseConnectionException, HydrawiseAuthenticationException, HydrawiseCommandException {
211         sendGraphQLMutation(String.format(MUTATION_SUSPEND_ZONE, relayId, until));
212     }
213
214     /**
215      * Resumes a given relay
216      *
217      * @param relayId
218      * @throws HydrawiseConnectionException
219      * @throws HydrawiseAuthenticationException
220      * @throws HydrawiseCommandException
221      */
222     public void resumeRelay(int relayId)
223             throws HydrawiseConnectionException, HydrawiseAuthenticationException, HydrawiseCommandException {
224         sendGraphQLMutation(String.format(MUTATION_RESUME_ZONE, relayId));
225     }
226
227     /**
228      * Suspend all relays on a given controller for an amount of seconds
229      *
230      * @param controllerId
231      * @param until
232      * @throws HydrawiseConnectionException
233      * @throws HydrawiseAuthenticationException
234      * @throws HydrawiseCommandException
235      */
236     public void suspendAllRelays(int controllerId, String until)
237             throws HydrawiseConnectionException, HydrawiseAuthenticationException, HydrawiseCommandException {
238         sendGraphQLMutation(String.format(MUTATION_SUSPEND_ALL_ZONES, controllerId, until));
239     }
240
241     /**
242      * Resumes all relays on a given controller
243      *
244      * @param controllerId
245      * @throws HydrawiseConnectionException
246      * @throws HydrawiseAuthenticationException
247      * @throws HydrawiseCommandException
248      */
249     public void resumeAllRelays(int controllerId)
250             throws HydrawiseConnectionException, HydrawiseAuthenticationException, HydrawiseCommandException {
251         sendGraphQLMutation(String.format(MUTATION_RESUME_ALL_ZONES, controllerId));
252     }
253
254     private String sendGraphQLQuery(String content)
255             throws HydrawiseConnectionException, HydrawiseAuthenticationException {
256         return sendGraphQLRequest(content);
257     }
258
259     private void sendGraphQLMutation(String content)
260             throws HydrawiseConnectionException, HydrawiseAuthenticationException, HydrawiseCommandException {
261         Mutation mutation = new Mutation(content);
262         logger.debug("Sending Mutation {}", gson.toJson(mutation).toString());
263         String response = sendGraphQLRequest(gson.toJson(mutation).toString());
264         logger.debug("Mutation response {}", response);
265         MutationResponse mResponse = gson.fromJson(response, MutationResponse.class);
266         if (mResponse == null) {
267             throw new HydrawiseCommandException("Malformed response: " + response);
268         }
269         Optional<MutationResponseStatus> status = mResponse.data.values().stream().findFirst();
270         if (!status.isPresent()) {
271             throw new HydrawiseCommandException("Unknown response: " + response);
272         }
273         if (status.get().status != StatusCode.OK) {
274             throw new HydrawiseCommandException("Command Status: " + status.get().status.name());
275         }
276     }
277
278     private String sendGraphQLRequest(String content)
279             throws HydrawiseConnectionException, HydrawiseAuthenticationException {
280         logger.trace("Sending Request: {}", content);
281         ContentResponse response;
282         final AtomicInteger responseCode = new AtomicInteger(0);
283         final StringBuilder responseMessage = new StringBuilder();
284         try {
285             AccessTokenResponse token = oAuthService.getAccessTokenResponse();
286             if (token == null) {
287                 throw new HydrawiseAuthenticationException("Login required");
288             }
289             response = httpClient.newRequest(GRAPH_URL).method(HttpMethod.POST)
290                     .content(new StringContentProvider(content), "application/json")
291                     .header("Authorization", token.getTokenType() + " " + token.getAccessToken())
292                     .onResponseFailure(new Response.FailureListener() {
293                         @Override
294                         public void onFailure(@Nullable Response response, @Nullable Throwable failure) {
295                             int status = response != null ? response.getStatus() : -1;
296                             String reason = response != null ? response.getReason() : "Null response";
297                             logger.trace("onFailure code: {} message: {}", status, reason);
298                             responseCode.set(status);
299                             responseMessage.append(reason);
300                         }
301                     }).send();
302             String stringResponse = response.getContentAsString();
303             logger.trace("Received Response: {}", stringResponse);
304             return stringResponse;
305         } catch (InterruptedException | TimeoutException | OAuthException | IOException e) {
306             logger.debug("Could not send request", e);
307             throw new HydrawiseConnectionException(e);
308         } catch (OAuthResponseException e) {
309             throw new HydrawiseAuthenticationException(e.getMessage());
310         } catch (ExecutionException e) {
311             // Hydrawise returns back a 40x status, but without a valid Realm , so jetty throws an exception,
312             // this allows us to catch this in a callback and handle accordingly
313             switch (responseCode.get()) {
314                 case 401:
315                 case 403:
316                     throw new HydrawiseAuthenticationException(responseMessage.toString());
317                 default:
318                     throw new HydrawiseConnectionException(e);
319             }
320         }
321     }
322
323     private String getQueryString() throws IOException {
324         if (queryString.isBlank()) {
325             try (InputStream inputStream = HydrawiseGraphQLClient.class.getClassLoader()
326                     .getResourceAsStream("query.graphql");
327                     BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream))) {
328                 queryString = bufferedReader.lines().collect(Collectors.joining("\n"));
329             }
330         }
331         return queryString;
332     }
333
334     class ResponseDeserializer<T> implements JsonDeserializer<T> {
335         @Override
336         @Nullable
337         public T deserialize(JsonElement je, Type type, JsonDeserializationContext jdc) throws JsonParseException {
338             return new Gson().fromJson(je, type);
339         }
340     }
341 }