]> git.basschouten.com Git - openhab-addons.git/blob
d407f1ced13f97988481097cbf1702f9bc6d556a
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 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.iaqualink.internal.api;
14
15 import java.io.IOException;
16 import java.lang.reflect.Type;
17 import java.net.URI;
18 import java.util.ArrayList;
19 import java.util.List;
20 import java.util.Objects;
21 import java.util.concurrent.ExecutionException;
22 import java.util.concurrent.TimeoutException;
23
24 import javax.ws.rs.core.UriBuilder;
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.util.StringContentProvider;
31 import org.eclipse.jetty.http.HttpHeader;
32 import org.eclipse.jetty.http.HttpMethod;
33 import org.eclipse.jetty.http.HttpStatus;
34 import org.openhab.binding.iaqualink.internal.api.model.AccountInfo;
35 import org.openhab.binding.iaqualink.internal.api.model.Auxiliary;
36 import org.openhab.binding.iaqualink.internal.api.model.Device;
37 import org.openhab.binding.iaqualink.internal.api.model.Home;
38 import org.openhab.binding.iaqualink.internal.api.model.OneTouch;
39 import org.openhab.binding.iaqualink.internal.api.model.SignIn;
40 import org.slf4j.Logger;
41 import org.slf4j.LoggerFactory;
42
43 import com.google.gson.FieldNamingPolicy;
44 import com.google.gson.Gson;
45 import com.google.gson.GsonBuilder;
46 import com.google.gson.JsonArray;
47 import com.google.gson.JsonDeserializationContext;
48 import com.google.gson.JsonDeserializer;
49 import com.google.gson.JsonElement;
50 import com.google.gson.JsonObject;
51 import com.google.gson.JsonParseException;
52 import com.google.gson.JsonPrimitive;
53
54 /**
55  * IAqualink HTTP Client
56  *
57  * The {@link IAqualinkClient} provides basic HTTP commands to control and monitor a iAquaLink
58  * based system.
59  *
60  * GSON is used to provide custom deserialization on the JSON results. These results
61  * unfortunately are not returned as normalized JSON objects and require complex deserialization
62  * handlers.
63  *
64  * @author Dan Cunningham - Initial contribution
65  *
66  */
67 @NonNullByDefault
68 public class IAqualinkClient {
69     private final Logger logger = LoggerFactory.getLogger(IAqualinkClient.class);
70
71     private static final String HEADER_AGENT = "iAqualink/98 CFNetwork/978.0.7 Darwin/18.6.0";
72     private static final String HEADER_ACCEPT = "*/*";
73     private static final String HEADER_ACCEPT_LANGUAGE = "en-us";
74     private static final String HEADER_ACCEPT_ENCODING = "br, gzip, deflate";
75
76     private static final String SUPPORT_URL = "https://support.iaqualink.com";
77     private static final String IAQUALINK_BASE_URL = "https://p-api.iaqualink.net/v1/mobile/session.json";
78
79     private Gson gson = new GsonBuilder().registerTypeAdapter(Home.class, new HomeDeserializer())
80             .registerTypeAdapter(OneTouch[].class, new OneTouchDeserializer())
81             .registerTypeAdapter(Auxiliary[].class, new AuxDeserializer())
82             .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create();
83     private Gson gsonInternal = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
84             .create();
85     private HttpClient httpClient;
86
87     @SuppressWarnings("serial")
88     public class NotAuthorizedException extends Exception {
89         public NotAuthorizedException(String message) {
90             super(message);
91         }
92     }
93
94     /**
95      *
96      * @param httpClient
97      */
98     public IAqualinkClient(HttpClient httpClient) {
99         this.httpClient = httpClient;
100     }
101
102     /**
103      * Initial login to service
104      *
105      * @param username
106      * @param password
107      * @param apiKey
108      * @return
109      * @throws IOException
110      * @throws NotAuthorizedException
111      */
112     public AccountInfo login(@Nullable String username, @Nullable String password, @Nullable String apiKey)
113             throws IOException, NotAuthorizedException {
114         String signIn = gson.toJson(new SignIn(apiKey, username, password)).toString();
115         try {
116             ContentResponse response = httpClient.newRequest(SUPPORT_URL + "/users/sign_in.json")
117                     .method(HttpMethod.POST).content(new StringContentProvider(signIn), "application/json").send();
118             if (response.getStatus() == HttpStatus.UNAUTHORIZED_401) {
119                 throw new NotAuthorizedException(response.getReason());
120             }
121             if (response.getStatus() != HttpStatus.OK_200) {
122                 throw new IOException(response.getReason());
123             }
124             return Objects.requireNonNull(gson.fromJson(response.getContentAsString(), AccountInfo.class));
125         } catch (InterruptedException | TimeoutException | ExecutionException e) {
126             throw new IOException(e);
127         }
128     }
129
130     /**
131      * List all devices (pools) registered to an account
132      *
133      * @param apiKey
134      * @param token
135      * @param id
136      * @return {@link Device[]}
137      * @throws IOException
138      * @throws NotAuthorizedException
139      */
140     public Device[] getDevices(@Nullable String apiKey, @Nullable String token, int id)
141             throws IOException, NotAuthorizedException {
142         return getAqualinkObject(UriBuilder.fromUri(SUPPORT_URL + "/devices.json"). //
143                 queryParam("api_key", apiKey). //
144                 queryParam("authentication_token", token). //
145                 queryParam("user_id", id).build(), Device[].class);
146     }
147
148     /**
149      * Retrieves the HomeScreen
150      *
151      * @param serialNumber
152      * @param sessionId
153      * @return {@link Home}
154      * @throws IOException
155      * @throws NotAuthorizedException
156      */
157     public Home getHome(@Nullable String serialNumber, @Nullable String sessionId)
158             throws IOException, NotAuthorizedException {
159         return homeScreenCommand(serialNumber, sessionId, "get_home");
160     }
161
162     /**
163      * Retrieves {@link OneTouch[]} macros
164      *
165      * @param serialNumber
166      * @param sessionId
167      * @return {@link OneTouch[]}
168      * @throws IOException
169      * @throws NotAuthorizedException
170      */
171     public OneTouch[] getOneTouch(@Nullable String serialNumber, @Nullable String sessionId)
172             throws IOException, NotAuthorizedException {
173         return oneTouchCommand(serialNumber, sessionId, "get_onetouch");
174     }
175
176     /**
177      * Retrieves {@link Auxiliary[]} devices
178      *
179      * @param serialNumber
180      * @param sessionId
181      * @return {@link Auxiliary[]}
182      * @throws IOException
183      * @throws NotAuthorizedException
184      */
185     public Auxiliary[] getAux(@Nullable String serial, @Nullable String sessionID)
186             throws IOException, NotAuthorizedException {
187         return auxCommand(serial, sessionID, "get_devices");
188     }
189
190     /**
191      * Sends a HomeScreen Set command
192      *
193      * @param serial
194      * @param sessionID
195      * @param homeElementID
196      * @return
197      * @throws IOException
198      * @throws NotAuthorizedException
199      */
200     public Home homeScreenSetCommand(@Nullable String serial, @Nullable String sessionID, String homeElementID)
201             throws IOException, NotAuthorizedException {
202         return homeScreenCommand(serial, sessionID, "set_" + homeElementID);
203     }
204
205     /**
206      * Sends an Auxiliary Set command
207      *
208      * @param serial
209      * @param sessionID
210      * @param auxID
211      * @return
212      * @throws IOException
213      * @throws NotAuthorizedException
214      */
215     public Auxiliary[] auxSetCommand(@Nullable String serial, @Nullable String sessionID, String auxID)
216             throws IOException, NotAuthorizedException {
217         return auxCommand(serial, sessionID, "set_" + auxID);
218     }
219
220     /**
221      * Sends an Auxiliary light command
222      *
223      * @param serialNumber
224      * @param sessionId
225      * @param command
226      * @param lightValue
227      * @return
228      * @throws IOException
229      * @throws NotAuthorizedException
230      */
231     public Auxiliary[] lightCommand(@Nullable String serial, @Nullable String sessionID, String auxID,
232             String lightValue, String subType) throws IOException, NotAuthorizedException {
233         return getAqualinkObject(baseURI(). //
234                 queryParam("aux", auxID). //
235                 queryParam("command", "set_light"). //
236                 queryParam("light", lightValue). //
237                 queryParam("serial", serial). //
238                 queryParam("subtype", subType). //
239                 queryParam("sessionID", sessionID).build(), Auxiliary[].class);
240     }
241
242     /**
243      * Sends a Auxiliary dimmer command
244      *
245      * @param serialNumber
246      * @param sessionId
247      * @param auxId
248      * @param lightValue
249      * @return
250      * @throws IOException
251      * @throws NotAuthorizedException
252      */
253     public Auxiliary[] dimmerCommand(@Nullable String serial, @Nullable String sessionID, String auxID, String level)
254             throws IOException, NotAuthorizedException {
255         return getAqualinkObject(baseURI().queryParam("aux", auxID). //
256                 queryParam("command", "set_dimmer"). //
257                 queryParam("level", level). //
258                 queryParam("serial", serial). //
259                 queryParam("sessionID", sessionID).build(), Auxiliary[].class);
260     }
261
262     /**
263      * Sets the Spa Temperature Setpoint
264      *
265      * @param serialNumber
266      * @param sessionId
267      * @param spaSetpoint
268      * @return
269      * @throws IOException
270      * @throws NotAuthorizedException
271      */
272     public Home setSpaTemp(@Nullable String serial, @Nullable String sessionID, float spaSetpoint)
273             throws IOException, NotAuthorizedException {
274         return getAqualinkObject(baseURI(). //
275                 queryParam("command", "set_temps"). //
276                 queryParam("temp1", spaSetpoint). //
277                 queryParam("serial", serial). //
278                 queryParam("sessionID", sessionID).build(), Home.class);
279     }
280
281     /**
282      * Sets the Pool Temperature Setpoint
283      *
284      * @param serialNumber
285      * @param sessionId
286      * @param poolSetpoint
287      * @return
288      * @throws IOException
289      * @throws NotAuthorizedException
290      */
291     public Home setPoolTemp(@Nullable String serial, @Nullable String sessionID, float poolSetpoint)
292             throws IOException, NotAuthorizedException {
293         return getAqualinkObject(baseURI(). //
294                 queryParam("command", "set_temps"). //
295                 queryParam("temp2", poolSetpoint). //
296                 queryParam("serial", serial). //
297                 queryParam("sessionID", sessionID).build(), Home.class);
298     }
299
300     /**
301      * Sends a OneTouch set command
302      *
303      * @param serial
304      * @param sessionID
305      * @param oneTouchID
306      * @return
307      * @throws IOException
308      * @throws NotAuthorizedException
309      */
310     public OneTouch[] oneTouchSetCommand(@Nullable String serial, @Nullable String sessionID, String oneTouchID)
311             throws IOException, NotAuthorizedException {
312         return oneTouchCommand(serial, sessionID, "set_" + oneTouchID);
313     }
314
315     private Home homeScreenCommand(@Nullable String serial, @Nullable String sessionID, String command)
316             throws IOException, NotAuthorizedException {
317         return getAqualinkObject(baseURI().queryParam("command", command). //
318                 queryParam("serial", serial). //
319                 queryParam("sessionID", sessionID).build(), Home.class);
320     }
321
322     private Auxiliary[] auxCommand(@Nullable String serial, @Nullable String sessionID, String command)
323             throws IOException, NotAuthorizedException {
324         return getAqualinkObject(baseURI(). //
325                 queryParam("command", command). //
326                 queryParam("serial", serial). //
327                 queryParam("sessionID", sessionID).build(), Auxiliary[].class);
328     }
329
330     private OneTouch[] oneTouchCommand(@Nullable String serial, @Nullable String sessionID, String command)
331             throws IOException, NotAuthorizedException {
332         return getAqualinkObject(baseURI().queryParam("command", command). //
333                 queryParam("serial", serial). //
334                 queryParam("sessionID", sessionID).build(), OneTouch[].class);
335     }
336
337     private UriBuilder baseURI() {
338         return UriBuilder.fromUri(IAQUALINK_BASE_URL).queryParam("actionID", "command");
339     }
340
341     /**
342      *
343      * @param <T>
344      * @param url
345      * @param typeOfT
346      * @return
347      * @throws IOException
348      * @throws NotAuthorizedException
349      */
350     private <T> T getAqualinkObject(URI uri, Type typeOfT) throws IOException, NotAuthorizedException {
351         return Objects.requireNonNull(gson.fromJson(getRequest(uri), typeOfT));
352     }
353
354     /**
355      *
356      * @param url
357      * @return
358      * @throws IOException
359      * @throws NotAuthorizedException
360      */
361     private String getRequest(URI uri) throws IOException, NotAuthorizedException {
362         try {
363             logger.trace("Trying {}", uri);
364             ContentResponse response = httpClient.newRequest(uri).method(HttpMethod.GET) //
365                     .agent(HEADER_AGENT) //
366                     .header(HttpHeader.ACCEPT_LANGUAGE, HEADER_ACCEPT_LANGUAGE) //
367                     .header(HttpHeader.ACCEPT_ENCODING, HEADER_ACCEPT_ENCODING) //
368                     .header(HttpHeader.ACCEPT, HEADER_ACCEPT) //
369                     .send();
370             logger.trace("Response {}", response);
371             if (response.getStatus() == HttpStatus.UNAUTHORIZED_401) {
372                 throw new NotAuthorizedException(response.getReason());
373             }
374             if (response.getStatus() != HttpStatus.OK_200) {
375                 throw new IOException(response.getReason());
376             }
377             return response.getContentAsString();
378         } catch (InterruptedException | TimeoutException | ExecutionException | JsonParseException e) {
379             throw new IOException(e);
380         }
381     }
382
383     /////////////// .........Here be dragons...../////////////////////////
384
385     class HomeDeserializer implements JsonDeserializer<Home> {
386         @Override
387         public @Nullable Home deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
388                 throws JsonParseException {
389             JsonObject jsonObject = json.getAsJsonObject();
390             JsonArray homeScreen = jsonObject.getAsJsonArray("home_screen");
391             JsonObject home = new JsonObject();
392             JsonObject serializedMap = new JsonObject();
393             if (homeScreen != null) {
394                 homeScreen.forEach(element -> {
395                     element.getAsJsonObject().entrySet().forEach(entry -> {
396                         home.add(entry.getKey(), entry.getValue());
397                         serializedMap.add(entry.getKey(), entry.getValue());
398                     });
399                 });
400                 home.add("serialized_map", serializedMap);
401                 return gsonInternal.fromJson(home, Home.class);
402             }
403             throw new JsonParseException("Invalid structure for Home class");
404         }
405     }
406
407     class OneTouchDeserializer implements JsonDeserializer<OneTouch[]> {
408         @Override
409         public OneTouch @Nullable [] deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
410                 throws JsonParseException {
411             JsonObject jsonObject = json.getAsJsonObject();
412             JsonArray oneTouchScreen = jsonObject.getAsJsonArray("onetouch_screen");
413             List<OneTouch> list = new ArrayList<>();
414             if (oneTouchScreen != null) {
415                 oneTouchScreen.forEach(oneTouchScreenElement -> {
416                     oneTouchScreenElement.getAsJsonObject().entrySet().forEach(oneTouchScreenEntry -> {
417                         if (oneTouchScreenEntry.getKey().startsWith("onetouch_")) {
418                             JsonArray oneTouchArray = oneTouchScreenEntry.getValue().getAsJsonArray();
419                             if (oneTouchArray != null) {
420                                 JsonObject oneTouchJson = new JsonObject();
421                                 oneTouchJson.add("name", new JsonPrimitive(oneTouchScreenEntry.getKey()));
422                                 oneTouchArray.forEach(arrayElement -> {
423                                     arrayElement.getAsJsonObject().entrySet().forEach(oneTouchEntry -> {
424                                         oneTouchJson.add(oneTouchEntry.getKey(), oneTouchEntry.getValue());
425                                     });
426                                 });
427                                 list.add(Objects.requireNonNull(gsonInternal.fromJson(oneTouchJson, OneTouch.class)));
428                             }
429                         }
430                     });
431                 });
432             }
433             return list.toArray(new OneTouch[list.size()]);
434         }
435     }
436
437     class AuxDeserializer implements JsonDeserializer<Auxiliary[]> {
438         @Override
439         public Auxiliary @Nullable [] deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
440                 throws JsonParseException {
441             JsonObject jsonObject = json.getAsJsonObject();
442             JsonArray auxScreen = jsonObject.getAsJsonArray("devices_screen");
443             List<Auxiliary> list = new ArrayList<>();
444             if (auxScreen != null) {
445                 auxScreen.forEach(auxElement -> {
446                     auxElement.getAsJsonObject().entrySet().forEach(auxScreenEntry -> {
447                         if (auxScreenEntry.getKey().startsWith("aux_")) {
448                             JsonArray auxArray = auxScreenEntry.getValue().getAsJsonArray();
449                             if (auxArray != null) {
450                                 JsonObject auxJson = new JsonObject();
451                                 auxJson.add("name", new JsonPrimitive(auxScreenEntry.getKey()));
452                                 auxArray.forEach(arrayElement -> {
453                                     arrayElement.getAsJsonObject().entrySet().forEach(auxEntry -> {
454                                         auxJson.add(auxEntry.getKey(), auxEntry.getValue());
455                                     });
456                                 });
457                                 list.add(Objects.requireNonNull(gsonInternal.fromJson(auxJson, Auxiliary.class)));
458                             }
459                         }
460                     });
461                 });
462             }
463             return list.toArray(new Auxiliary[list.size()]);
464         }
465     }
466 }