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