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