]> git.basschouten.com Git - openhab-addons.git/blob
daf280ca8a327f10060ceb36f7f7ba9f8f80ad93
[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.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.dto.AccountInfo;
35 import org.openhab.binding.iaqualink.internal.api.dto.Auxiliary;
36 import org.openhab.binding.iaqualink.internal.api.dto.Device;
37 import org.openhab.binding.iaqualink.internal.api.dto.Home;
38 import org.openhab.binding.iaqualink.internal.api.dto.OneTouch;
39 import org.openhab.binding.iaqualink.internal.api.dto.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 an 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 AUTH_URL = "https://prod.zodiac-io.com/users/v1/login";
77     private static final String DEVICES_URL = "https://r-api.iaqualink.net/devices.json";
78     private static final String IAQUALINK_BASE_URL = "https://p-api.iaqualink.net/v1/mobile/session.json";
79
80     private Gson gson = new GsonBuilder().registerTypeAdapter(Home.class, new HomeDeserializer())
81             .registerTypeAdapter(OneTouch[].class, new OneTouchDeserializer())
82             .registerTypeAdapter(Auxiliary[].class, new AuxDeserializer())
83             .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create();
84     private Gson gsonInternal = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
85             .create();
86     private HttpClient httpClient;
87
88     @SuppressWarnings("serial")
89     public class NotAuthorizedException extends Exception {
90         public NotAuthorizedException(String message) {
91             super(message);
92         }
93     }
94
95     /**
96      *
97      * @param httpClient
98      */
99     public IAqualinkClient(HttpClient httpClient) {
100         this.httpClient = httpClient;
101     }
102
103     /**
104      * Initial login to service
105      *
106      * @param username
107      * @param password
108      * @param apiKey
109      * @return
110      * @throws IOException
111      * @throws NotAuthorizedException
112      */
113     public AccountInfo login(@Nullable String username, @Nullable String password, @Nullable String apiKey)
114             throws IOException, NotAuthorizedException {
115         String signIn = gson.toJson(new SignIn(apiKey, username, password)).toString();
116         try {
117             ContentResponse response = httpClient.newRequest(AUTH_URL).method(HttpMethod.POST)
118                     .content(new StringContentProvider(signIn), "application/json").send();
119             if (response.getStatus() == HttpStatus.UNAUTHORIZED_401) {
120                 throw new NotAuthorizedException(response.getReason());
121             }
122             if (response.getStatus() != HttpStatus.OK_200) {
123                 throw new IOException(response.getReason());
124             }
125             return Objects.requireNonNull(gson.fromJson(response.getContentAsString(), AccountInfo.class));
126         } catch (InterruptedException | TimeoutException | ExecutionException e) {
127             throw new IOException(e);
128         }
129     }
130
131     /**
132      * List all devices (pools) registered to an account
133      *
134      * @param apiKey
135      * @param token
136      * @param id
137      * @return {@link Device[]}
138      * @throws IOException
139      * @throws NotAuthorizedException
140      */
141     public Device[] getDevices(@Nullable String apiKey, @Nullable String token, int id)
142             throws IOException, NotAuthorizedException {
143         return getAqualinkObject(UriBuilder.fromUri(DEVICES_URL). //
144                 queryParam("api_key", apiKey). //
145                 queryParam("authentication_token", token). //
146                 queryParam("user_id", id).build(), Device[].class);
147     }
148
149     /**
150      * Retrieves the HomeScreen
151      *
152      * @param serialNumber
153      * @param sessionId
154      * @return {@link Home}
155      * @throws IOException
156      * @throws NotAuthorizedException
157      */
158     public Home getHome(@Nullable String serialNumber, @Nullable String sessionId)
159             throws IOException, NotAuthorizedException {
160         return homeScreenCommand(serialNumber, sessionId, "get_home");
161     }
162
163     /**
164      * Retrieves {@link OneTouch[]} macros
165      *
166      * @param serialNumber
167      * @param sessionId
168      * @return {@link OneTouch[]}
169      * @throws IOException
170      * @throws NotAuthorizedException
171      */
172     public OneTouch[] getOneTouch(@Nullable String serialNumber, @Nullable String sessionId)
173             throws IOException, NotAuthorizedException {
174         return oneTouchCommand(serialNumber, sessionId, "get_onetouch");
175     }
176
177     /**
178      * Retrieves {@link Auxiliary[]} devices
179      *
180      * @param serial
181      * @param sessionID
182      * @return {@link Auxiliary[]}
183      * @throws IOException
184      * @throws NotAuthorizedException
185      */
186     public Auxiliary[] getAux(@Nullable String serial, @Nullable String sessionID)
187             throws IOException, NotAuthorizedException {
188         return auxCommand(serial, sessionID, "get_devices");
189     }
190
191     /**
192      * Sends a HomeScreen Set command
193      *
194      * @param serial
195      * @param sessionID
196      * @param homeElementID
197      * @return
198      * @throws IOException
199      * @throws NotAuthorizedException
200      */
201     public Home homeScreenSetCommand(@Nullable String serial, @Nullable String sessionID, String homeElementID)
202             throws IOException, NotAuthorizedException {
203         return homeScreenCommand(serial, sessionID, "set_" + homeElementID);
204     }
205
206     /**
207      * Sends an Auxiliary Set command
208      *
209      * @param serial
210      * @param sessionID
211      * @param auxID
212      * @return
213      * @throws IOException
214      * @throws NotAuthorizedException
215      */
216     public Auxiliary[] auxSetCommand(@Nullable String serial, @Nullable String sessionID, String auxID)
217             throws IOException, NotAuthorizedException {
218         return auxCommand(serial, sessionID, "set_" + auxID);
219     }
220
221     /**
222      * Sends an Auxiliary light command
223      *
224      * @param serial
225      * @param sessionID
226      * @param auxID
227      * @param lightValue
228      * @param subType
229      * @return
230      * @throws IOException
231      * @throws NotAuthorizedException
232      */
233     public Auxiliary[] lightCommand(@Nullable String serial, @Nullable String sessionID, String auxID,
234             String lightValue, String subType) throws IOException, NotAuthorizedException {
235         return getAqualinkObject(baseURI(). //
236                 queryParam("aux", auxID). //
237                 queryParam("command", "set_light"). //
238                 queryParam("light", lightValue). //
239                 queryParam("serial", serial). //
240                 queryParam("subtype", subType). //
241                 queryParam("sessionID", sessionID).build(), Auxiliary[].class);
242     }
243
244     /**
245      * Sends an Auxiliary dimmer command
246      *
247      * @param serial
248      * @param sessionID
249      * @param auxID
250      * @param level
251      * @return
252      * @throws IOException
253      * @throws NotAuthorizedException
254      */
255     public Auxiliary[] dimmerCommand(@Nullable String serial, @Nullable String sessionID, String auxID, String level)
256             throws IOException, NotAuthorizedException {
257         return getAqualinkObject(baseURI().queryParam("aux", auxID). //
258                 queryParam("command", "set_dimmer"). //
259                 queryParam("level", level). //
260                 queryParam("serial", serial). //
261                 queryParam("sessionID", sessionID).build(), Auxiliary[].class);
262     }
263
264     /**
265      * Sets the Spa Temperature Setpoint
266      *
267      * @param serial
268      * @param sessionID
269      * @param spaSetpoint
270      * @return
271      * @throws IOException
272      * @throws NotAuthorizedException
273      */
274     public Home setSpaTemp(@Nullable String serial, @Nullable String sessionID, float spaSetpoint)
275             throws IOException, NotAuthorizedException {
276         return getAqualinkObject(baseURI(). //
277                 queryParam("command", "set_temps"). //
278                 queryParam("temp1", spaSetpoint). //
279                 queryParam("serial", serial). //
280                 queryParam("sessionID", sessionID).build(), Home.class);
281     }
282
283     /**
284      * Sets the Pool Temperature Setpoint
285      *
286      * @param serial
287      * @param sessionID
288      * @param poolSetpoint
289      * @return
290      * @throws IOException
291      * @throws NotAuthorizedException
292      */
293     public Home setPoolTemp(@Nullable String serial, @Nullable String sessionID, float poolSetpoint)
294             throws IOException, NotAuthorizedException {
295         return getAqualinkObject(baseURI(). //
296                 queryParam("command", "set_temps"). //
297                 queryParam("temp2", poolSetpoint). //
298                 queryParam("serial", serial). //
299                 queryParam("sessionID", sessionID).build(), Home.class);
300     }
301
302     /**
303      * Sends a OneTouch set command
304      *
305      * @param serial
306      * @param sessionID
307      * @param oneTouchID
308      * @return
309      * @throws IOException
310      * @throws NotAuthorizedException
311      */
312     public OneTouch[] oneTouchSetCommand(@Nullable String serial, @Nullable String sessionID, String oneTouchID)
313             throws IOException, NotAuthorizedException {
314         return oneTouchCommand(serial, sessionID, "set_" + oneTouchID);
315     }
316
317     private Home homeScreenCommand(@Nullable String serial, @Nullable String sessionID, String command)
318             throws IOException, NotAuthorizedException {
319         return getAqualinkObject(baseURI().queryParam("command", command). //
320                 queryParam("serial", serial). //
321                 queryParam("sessionID", sessionID).build(), Home.class);
322     }
323
324     private Auxiliary[] auxCommand(@Nullable String serial, @Nullable String sessionID, String command)
325             throws IOException, NotAuthorizedException {
326         return getAqualinkObject(baseURI(). //
327                 queryParam("command", command). //
328                 queryParam("serial", serial). //
329                 queryParam("sessionID", sessionID).build(), Auxiliary[].class);
330     }
331
332     private OneTouch[] oneTouchCommand(@Nullable String serial, @Nullable String sessionID, String command)
333             throws IOException, NotAuthorizedException {
334         return getAqualinkObject(baseURI().queryParam("command", command). //
335                 queryParam("serial", serial). //
336                 queryParam("sessionID", sessionID).build(), OneTouch[].class);
337     }
338
339     private UriBuilder baseURI() {
340         return UriBuilder.fromUri(IAQUALINK_BASE_URL).queryParam("actionID", "command");
341     }
342
343     /**
344      *
345      * @param <T>
346      * @param url
347      * @param typeOfT
348      * @return
349      * @throws IOException
350      * @throws NotAuthorizedException
351      */
352     private <T> T getAqualinkObject(URI uri, Type typeOfT) throws IOException, NotAuthorizedException {
353         return Objects.requireNonNull(gson.fromJson(getRequest(uri), typeOfT));
354     }
355
356     /**
357      *
358      * @param url
359      * @return
360      * @throws IOException
361      * @throws NotAuthorizedException
362      */
363     private String getRequest(URI uri) throws IOException, NotAuthorizedException {
364         try {
365             logger.trace("Trying {}", uri);
366             ContentResponse response = httpClient.newRequest(uri).method(HttpMethod.GET) //
367                     .agent(HEADER_AGENT) //
368                     .header(HttpHeader.ACCEPT_LANGUAGE, HEADER_ACCEPT_LANGUAGE) //
369                     .header(HttpHeader.ACCEPT_ENCODING, HEADER_ACCEPT_ENCODING) //
370                     .header(HttpHeader.ACCEPT, HEADER_ACCEPT) //
371                     .send();
372             logger.trace("Response {}", response);
373             if (response.getStatus() == HttpStatus.UNAUTHORIZED_401) {
374                 throw new NotAuthorizedException(response.getReason());
375             }
376             if (response.getStatus() != HttpStatus.OK_200) {
377                 throw new IOException(response.getReason());
378             }
379             return response.getContentAsString();
380         } catch (InterruptedException | TimeoutException | ExecutionException | JsonParseException e) {
381             throw new IOException(e);
382         }
383     }
384
385     /////////////// .........Here be dragons...../////////////////////////
386
387     class HomeDeserializer implements JsonDeserializer<Home> {
388         @Override
389         public @Nullable Home deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
390                 throws JsonParseException {
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                         JsonElement value = entry.getValue();
399                         home.add(entry.getKey(), value);
400                         if (value.isJsonPrimitive() && value.getAsJsonPrimitive().isString()) {
401                             serializedMap.add(entry.getKey(), value);
402                         }
403                     });
404                 });
405                 home.add("serialized_map", serializedMap);
406                 return gsonInternal.fromJson(home, Home.class);
407             }
408             throw new JsonParseException("Invalid structure for Home class");
409         }
410     }
411
412     class OneTouchDeserializer implements JsonDeserializer<OneTouch[]> {
413         @Override
414         public OneTouch @Nullable [] deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
415                 throws JsonParseException {
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(Objects.requireNonNull(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 @Nullable [] deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
445                 throws JsonParseException {
446             JsonObject jsonObject = json.getAsJsonObject();
447             JsonArray auxScreen = jsonObject.getAsJsonArray("devices_screen");
448             List<Auxiliary> list = new ArrayList<>();
449             if (auxScreen != null) {
450                 auxScreen.forEach(auxElement -> {
451                     auxElement.getAsJsonObject().entrySet().forEach(auxScreenEntry -> {
452                         if (auxScreenEntry.getKey().startsWith("aux_")) {
453                             JsonArray auxArray = auxScreenEntry.getValue().getAsJsonArray();
454                             if (auxArray != null) {
455                                 JsonObject auxJson = new JsonObject();
456                                 auxJson.add("name", new JsonPrimitive(auxScreenEntry.getKey()));
457                                 auxArray.forEach(arrayElement -> {
458                                     arrayElement.getAsJsonObject().entrySet().forEach(auxEntry -> {
459                                         auxJson.add(auxEntry.getKey(), auxEntry.getValue());
460                                     });
461                                 });
462                                 list.add(Objects.requireNonNull(gsonInternal.fromJson(auxJson, Auxiliary.class)));
463                             }
464                         }
465                     });
466                 });
467             }
468             return list.toArray(new Auxiliary[list.size()]);
469         }
470     }
471 }