2 * Copyright (c) 2010-2020 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.iaqualink.internal.api;
15 import java.io.IOException;
16 import java.lang.reflect.Type;
18 import java.util.ArrayList;
19 import java.util.List;
20 import java.util.concurrent.ExecutionException;
21 import java.util.concurrent.TimeoutException;
23 import javax.ws.rs.core.UriBuilder;
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;
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;
54 * IAqualink HTTP Client
56 * The {@link IAqualinkClient} provides basic HTTP commands to control and monitor a iAquaLink
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
63 * @author Dan Cunningham - Initial contribution
67 public class IAqualinkClient {
68 private final Logger logger = LoggerFactory.getLogger(IAqualinkClient.class);
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";
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";
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)
84 private HttpClient httpClient;
86 @SuppressWarnings("serial")
87 public class NotAuthorizedException extends Exception {
88 public NotAuthorizedException(String message) {
97 public IAqualinkClient(HttpClient httpClient) {
98 this.httpClient = httpClient;
102 * Initial login to service
108 * @throws IOException
109 * @throws NotAuthorizedException
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();
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());
120 if (response.getStatus() != HttpStatus.OK_200) {
121 throw new IOException(response.getReason());
123 return gson.fromJson(response.getContentAsString(), AccountInfo.class);
124 } catch (InterruptedException | TimeoutException | ExecutionException e) {
125 throw new IOException(e);
130 * List all devices (pools) registered to an account
135 * @return {@link Device[]}
136 * @throws IOException
137 * @throws NotAuthorizedException
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);
148 * Retrieves the HomeScreen
150 * @param serialNumber
152 * @return {@link Home}
153 * @throws IOException
154 * @throws NotAuthorizedException
156 public Home getHome(@Nullable String serialNumber, @Nullable String sessionId)
157 throws IOException, NotAuthorizedException {
158 return homeScreenCommand(serialNumber, sessionId, "get_home");
162 * Retrieves {@link OneTouch[]} macros
164 * @param serialNumber
166 * @return {@link OneTouch[]}
167 * @throws IOException
168 * @throws NotAuthorizedException
170 public OneTouch[] getOneTouch(@Nullable String serialNumber, @Nullable String sessionId)
171 throws IOException, NotAuthorizedException {
172 return oneTouchCommand(serialNumber, sessionId, "get_onetouch");
176 * Retrieves {@link Auxiliary[]} devices
178 * @param serialNumber
180 * @return {@link Auxiliary[]}
181 * @throws IOException
182 * @throws NotAuthorizedException
184 public Auxiliary[] getAux(@Nullable String serial, @Nullable String sessionID)
185 throws IOException, NotAuthorizedException {
186 return auxCommand(serial, sessionID, "get_devices");
190 * Sends a HomeScreen Set command
194 * @param homeElementID
196 * @throws IOException
197 * @throws NotAuthorizedException
199 public Home homeScreenSetCommand(@Nullable String serial, @Nullable String sessionID, String homeElementID)
200 throws IOException, NotAuthorizedException {
201 return homeScreenCommand(serial, sessionID, "set_" + homeElementID);
205 * Sends an Auxiliary Set command
211 * @throws IOException
212 * @throws NotAuthorizedException
214 public Auxiliary[] auxSetCommand(@Nullable String serial, @Nullable String sessionID, String auxID)
215 throws IOException, NotAuthorizedException {
216 return auxCommand(serial, sessionID, "set_" + auxID);
220 * Sends an Auxiliary light command
222 * @param serialNumber
227 * @throws IOException
228 * @throws NotAuthorizedException
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);
242 * Sends a Auxiliary dimmer command
244 * @param serialNumber
249 * @throws IOException
250 * @throws NotAuthorizedException
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);
262 * Sets the Spa Temperature Setpoint
264 * @param serialNumber
268 * @throws IOException
269 * @throws NotAuthorizedException
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);
281 * Sets the Pool Temperature Setpoint
283 * @param serialNumber
285 * @param poolSetpoint
287 * @throws IOException
288 * @throws NotAuthorizedException
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);
300 * Sends a OneTouch set command
306 * @throws IOException
307 * @throws NotAuthorizedException
309 public OneTouch[] oneTouchSetCommand(@Nullable String serial, @Nullable String sessionID, String oneTouchID)
310 throws IOException, NotAuthorizedException {
311 return oneTouchCommand(serial, sessionID, "set_" + oneTouchID);
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);
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);
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);
336 private UriBuilder baseURI() {
337 return UriBuilder.fromUri(IAQUALINK_BASE_URL).queryParam("actionID", "command");
346 * @throws IOException
347 * @throws NotAuthorizedException
349 private <T> T getAqualinkObject(URI uri, Type typeOfT) throws IOException, NotAuthorizedException {
350 return gson.fromJson(getRequest(uri), typeOfT);
357 * @throws IOException
358 * @throws NotAuthorizedException
360 private String getRequest(URI uri) throws IOException, NotAuthorizedException {
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) //
369 logger.trace("Response {}", response);
370 if (response.getStatus() == HttpStatus.UNAUTHORIZED_401) {
371 throw new NotAuthorizedException(response.getReason());
373 if (response.getStatus() != HttpStatus.OK_200) {
374 throw new IOException(response.getReason());
376 return response.getContentAsString();
377 } catch (InterruptedException | TimeoutException | ExecutionException | JsonParseException e) {
378 throw new IOException(e);
382 /////////////// .........Here be dragons...../////////////////////////
384 class HomeDeserializer implements JsonDeserializer<Home> {
386 public Home deserialize(@Nullable JsonElement json, @Nullable Type typeOfT,
387 @Nullable JsonDeserializationContext context) throws JsonParseException {
389 throw new JsonParseException("No JSON");
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());
402 home.add("serialized_map", serializedMap);
403 return gsonInternal.fromJson(home, Home.class);
405 throw new JsonParseException("Invalid structure for Home class");
409 class OneTouchDeserializer implements JsonDeserializer<OneTouch[]> {
411 public OneTouch[] deserialize(@Nullable JsonElement json, @Nullable Type typeOfT,
412 @Nullable JsonDeserializationContext context) throws JsonParseException {
414 throw new JsonParseException("No JSON");
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());
432 list.add(gsonInternal.fromJson(oneTouchJson, OneTouch.class));
438 return list.toArray(new OneTouch[list.size()]);
442 class AuxDeserializer implements JsonDeserializer<Auxiliary[]> {
444 public Auxiliary[] deserialize(@Nullable JsonElement json, @Nullable Type typeOfT,
445 @Nullable JsonDeserializationContext context) throws JsonParseException {
447 throw new JsonParseException("No JSON");
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());
465 list.add(gsonInternal.fromJson(auxJson, Auxiliary.class));
471 return list.toArray(new Auxiliary[list.size()]);