]> git.basschouten.com Git - openhab-addons.git/blob
f8dd96fb85359e40ad9f32e9f6e7be81a938abf6
[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 serialNumber
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 serialNumber
225      * @param sessionId
226      * @param command
227      * @param lightValue
228      * @return
229      * @throws IOException
230      * @throws NotAuthorizedException
231      */
232     public Auxiliary[] lightCommand(@Nullable String serial, @Nullable String sessionID, String auxID,
233             String lightValue, String subType) throws IOException, NotAuthorizedException {
234         return getAqualinkObject(baseURI(). //
235                 queryParam("aux", auxID). //
236                 queryParam("command", "set_light"). //
237                 queryParam("light", lightValue). //
238                 queryParam("serial", serial). //
239                 queryParam("subtype", subType). //
240                 queryParam("sessionID", sessionID).build(), Auxiliary[].class);
241     }
242
243     /**
244      * Sends an Auxiliary dimmer command
245      *
246      * @param serialNumber
247      * @param sessionId
248      * @param auxId
249      * @param lightValue
250      * @return
251      * @throws IOException
252      * @throws NotAuthorizedException
253      */
254     public Auxiliary[] dimmerCommand(@Nullable String serial, @Nullable String sessionID, String auxID, String level)
255             throws IOException, NotAuthorizedException {
256         return getAqualinkObject(baseURI().queryParam("aux", auxID). //
257                 queryParam("command", "set_dimmer"). //
258                 queryParam("level", level). //
259                 queryParam("serial", serial). //
260                 queryParam("sessionID", sessionID).build(), Auxiliary[].class);
261     }
262
263     /**
264      * Sets the Spa Temperature Setpoint
265      *
266      * @param serialNumber
267      * @param sessionId
268      * @param spaSetpoint
269      * @return
270      * @throws IOException
271      * @throws NotAuthorizedException
272      */
273     public Home setSpaTemp(@Nullable String serial, @Nullable String sessionID, float spaSetpoint)
274             throws IOException, NotAuthorizedException {
275         return getAqualinkObject(baseURI(). //
276                 queryParam("command", "set_temps"). //
277                 queryParam("temp1", spaSetpoint). //
278                 queryParam("serial", serial). //
279                 queryParam("sessionID", sessionID).build(), Home.class);
280     }
281
282     /**
283      * Sets the Pool Temperature Setpoint
284      *
285      * @param serialNumber
286      * @param sessionId
287      * @param poolSetpoint
288      * @return
289      * @throws IOException
290      * @throws NotAuthorizedException
291      */
292     public Home setPoolTemp(@Nullable String serial, @Nullable String sessionID, float poolSetpoint)
293             throws IOException, NotAuthorizedException {
294         return getAqualinkObject(baseURI(). //
295                 queryParam("command", "set_temps"). //
296                 queryParam("temp2", poolSetpoint). //
297                 queryParam("serial", serial). //
298                 queryParam("sessionID", sessionID).build(), Home.class);
299     }
300
301     /**
302      * Sends a OneTouch set command
303      *
304      * @param serial
305      * @param sessionID
306      * @param oneTouchID
307      * @return
308      * @throws IOException
309      * @throws NotAuthorizedException
310      */
311     public OneTouch[] oneTouchSetCommand(@Nullable String serial, @Nullable String sessionID, String oneTouchID)
312             throws IOException, NotAuthorizedException {
313         return oneTouchCommand(serial, sessionID, "set_" + oneTouchID);
314     }
315
316     private Home homeScreenCommand(@Nullable String serial, @Nullable String sessionID, String command)
317             throws IOException, NotAuthorizedException {
318         return getAqualinkObject(baseURI().queryParam("command", command). //
319                 queryParam("serial", serial). //
320                 queryParam("sessionID", sessionID).build(), Home.class);
321     }
322
323     private Auxiliary[] auxCommand(@Nullable String serial, @Nullable String sessionID, String command)
324             throws IOException, NotAuthorizedException {
325         return getAqualinkObject(baseURI(). //
326                 queryParam("command", command). //
327                 queryParam("serial", serial). //
328                 queryParam("sessionID", sessionID).build(), Auxiliary[].class);
329     }
330
331     private OneTouch[] oneTouchCommand(@Nullable String serial, @Nullable String sessionID, String command)
332             throws IOException, NotAuthorizedException {
333         return getAqualinkObject(baseURI().queryParam("command", command). //
334                 queryParam("serial", serial). //
335                 queryParam("sessionID", sessionID).build(), OneTouch[].class);
336     }
337
338     private UriBuilder baseURI() {
339         return UriBuilder.fromUri(IAQUALINK_BASE_URL).queryParam("actionID", "command");
340     }
341
342     /**
343      *
344      * @param <T>
345      * @param url
346      * @param typeOfT
347      * @return
348      * @throws IOException
349      * @throws NotAuthorizedException
350      */
351     private <T> T getAqualinkObject(URI uri, Type typeOfT) throws IOException, NotAuthorizedException {
352         return Objects.requireNonNull(gson.fromJson(getRequest(uri), typeOfT));
353     }
354
355     /**
356      *
357      * @param url
358      * @return
359      * @throws IOException
360      * @throws NotAuthorizedException
361      */
362     private String getRequest(URI uri) throws IOException, NotAuthorizedException {
363         try {
364             logger.trace("Trying {}", uri);
365             ContentResponse response = httpClient.newRequest(uri).method(HttpMethod.GET) //
366                     .agent(HEADER_AGENT) //
367                     .header(HttpHeader.ACCEPT_LANGUAGE, HEADER_ACCEPT_LANGUAGE) //
368                     .header(HttpHeader.ACCEPT_ENCODING, HEADER_ACCEPT_ENCODING) //
369                     .header(HttpHeader.ACCEPT, HEADER_ACCEPT) //
370                     .send();
371             logger.trace("Response {}", response);
372             if (response.getStatus() == HttpStatus.UNAUTHORIZED_401) {
373                 throw new NotAuthorizedException(response.getReason());
374             }
375             if (response.getStatus() != HttpStatus.OK_200) {
376                 throw new IOException(response.getReason());
377             }
378             return response.getContentAsString();
379         } catch (InterruptedException | TimeoutException | ExecutionException | JsonParseException e) {
380             throw new IOException(e);
381         }
382     }
383
384     /////////////// .........Here be dragons...../////////////////////////
385
386     class HomeDeserializer implements JsonDeserializer<Home> {
387         @Override
388         public @Nullable Home deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
389                 throws JsonParseException {
390             JsonObject jsonObject = json.getAsJsonObject();
391             JsonArray homeScreen = jsonObject.getAsJsonArray("home_screen");
392             JsonObject home = new JsonObject();
393             JsonObject serializedMap = new JsonObject();
394             if (homeScreen != null) {
395                 homeScreen.forEach(element -> {
396                     element.getAsJsonObject().entrySet().forEach(entry -> {
397                         JsonElement value = entry.getValue();
398                         home.add(entry.getKey(), value);
399                         if (value.isJsonPrimitive() && value.getAsJsonPrimitive().isString()) {
400                             serializedMap.add(entry.getKey(), value);
401                         }
402                     });
403                 });
404                 home.add("serialized_map", serializedMap);
405                 return gsonInternal.fromJson(home, Home.class);
406             }
407             throw new JsonParseException("Invalid structure for Home class");
408         }
409     }
410
411     class OneTouchDeserializer implements JsonDeserializer<OneTouch[]> {
412         @Override
413         public OneTouch @Nullable [] deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
414                 throws JsonParseException {
415             JsonObject jsonObject = json.getAsJsonObject();
416             JsonArray oneTouchScreen = jsonObject.getAsJsonArray("onetouch_screen");
417             List<OneTouch> list = new ArrayList<>();
418             if (oneTouchScreen != null) {
419                 oneTouchScreen.forEach(oneTouchScreenElement -> {
420                     oneTouchScreenElement.getAsJsonObject().entrySet().forEach(oneTouchScreenEntry -> {
421                         if (oneTouchScreenEntry.getKey().startsWith("onetouch_")) {
422                             JsonArray oneTouchArray = oneTouchScreenEntry.getValue().getAsJsonArray();
423                             if (oneTouchArray != null) {
424                                 JsonObject oneTouchJson = new JsonObject();
425                                 oneTouchJson.add("name", new JsonPrimitive(oneTouchScreenEntry.getKey()));
426                                 oneTouchArray.forEach(arrayElement -> {
427                                     arrayElement.getAsJsonObject().entrySet().forEach(oneTouchEntry -> {
428                                         oneTouchJson.add(oneTouchEntry.getKey(), oneTouchEntry.getValue());
429                                     });
430                                 });
431                                 list.add(Objects.requireNonNull(gsonInternal.fromJson(oneTouchJson, OneTouch.class)));
432                             }
433                         }
434                     });
435                 });
436             }
437             return list.toArray(new OneTouch[list.size()]);
438         }
439     }
440
441     class AuxDeserializer implements JsonDeserializer<Auxiliary[]> {
442         @Override
443         public Auxiliary @Nullable [] deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
444                 throws JsonParseException {
445             JsonObject jsonObject = json.getAsJsonObject();
446             JsonArray auxScreen = jsonObject.getAsJsonArray("devices_screen");
447             List<Auxiliary> list = new ArrayList<>();
448             if (auxScreen != null) {
449                 auxScreen.forEach(auxElement -> {
450                     auxElement.getAsJsonObject().entrySet().forEach(auxScreenEntry -> {
451                         if (auxScreenEntry.getKey().startsWith("aux_")) {
452                             JsonArray auxArray = auxScreenEntry.getValue().getAsJsonArray();
453                             if (auxArray != null) {
454                                 JsonObject auxJson = new JsonObject();
455                                 auxJson.add("name", new JsonPrimitive(auxScreenEntry.getKey()));
456                                 auxArray.forEach(arrayElement -> {
457                                     arrayElement.getAsJsonObject().entrySet().forEach(auxEntry -> {
458                                         auxJson.add(auxEntry.getKey(), auxEntry.getValue());
459                                     });
460                                 });
461                                 list.add(Objects.requireNonNull(gsonInternal.fromJson(auxJson, Auxiliary.class)));
462                             }
463                         }
464                     });
465                 });
466             }
467             return list.toArray(new Auxiliary[list.size()]);
468         }
469     }
470 }