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.Objects;
21 import java.util.concurrent.ExecutionException;
22 import java.util.concurrent.TimeoutException;
24 import javax.ws.rs.core.UriBuilder;
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;
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;
55 * IAqualink HTTP Client
57 * The {@link IAqualinkClient} provides basic HTTP commands to control and monitor a iAquaLink
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
64 * @author Dan Cunningham - Initial contribution
68 public class IAqualinkClient {
69 private final Logger logger = LoggerFactory.getLogger(IAqualinkClient.class);
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";
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";
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)
85 private HttpClient httpClient;
87 @SuppressWarnings("serial")
88 public class NotAuthorizedException extends Exception {
89 public NotAuthorizedException(String message) {
98 public IAqualinkClient(HttpClient httpClient) {
99 this.httpClient = httpClient;
103 * Initial login to service
109 * @throws IOException
110 * @throws NotAuthorizedException
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();
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());
121 if (response.getStatus() != HttpStatus.OK_200) {
122 throw new IOException(response.getReason());
124 return Objects.requireNonNull(gson.fromJson(response.getContentAsString(), AccountInfo.class));
125 } catch (InterruptedException | TimeoutException | ExecutionException e) {
126 throw new IOException(e);
131 * List all devices (pools) registered to an account
136 * @return {@link Device[]}
137 * @throws IOException
138 * @throws NotAuthorizedException
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);
149 * Retrieves the HomeScreen
151 * @param serialNumber
153 * @return {@link Home}
154 * @throws IOException
155 * @throws NotAuthorizedException
157 public Home getHome(@Nullable String serialNumber, @Nullable String sessionId)
158 throws IOException, NotAuthorizedException {
159 return homeScreenCommand(serialNumber, sessionId, "get_home");
163 * Retrieves {@link OneTouch[]} macros
165 * @param serialNumber
167 * @return {@link OneTouch[]}
168 * @throws IOException
169 * @throws NotAuthorizedException
171 public OneTouch[] getOneTouch(@Nullable String serialNumber, @Nullable String sessionId)
172 throws IOException, NotAuthorizedException {
173 return oneTouchCommand(serialNumber, sessionId, "get_onetouch");
177 * Retrieves {@link Auxiliary[]} devices
179 * @param serialNumber
181 * @return {@link Auxiliary[]}
182 * @throws IOException
183 * @throws NotAuthorizedException
185 public Auxiliary[] getAux(@Nullable String serial, @Nullable String sessionID)
186 throws IOException, NotAuthorizedException {
187 return auxCommand(serial, sessionID, "get_devices");
191 * Sends a HomeScreen Set command
195 * @param homeElementID
197 * @throws IOException
198 * @throws NotAuthorizedException
200 public Home homeScreenSetCommand(@Nullable String serial, @Nullable String sessionID, String homeElementID)
201 throws IOException, NotAuthorizedException {
202 return homeScreenCommand(serial, sessionID, "set_" + homeElementID);
206 * Sends an Auxiliary Set command
212 * @throws IOException
213 * @throws NotAuthorizedException
215 public Auxiliary[] auxSetCommand(@Nullable String serial, @Nullable String sessionID, String auxID)
216 throws IOException, NotAuthorizedException {
217 return auxCommand(serial, sessionID, "set_" + auxID);
221 * Sends an Auxiliary light command
223 * @param serialNumber
228 * @throws IOException
229 * @throws NotAuthorizedException
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);
243 * Sends a Auxiliary dimmer command
245 * @param serialNumber
250 * @throws IOException
251 * @throws NotAuthorizedException
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);
263 * Sets the Spa Temperature Setpoint
265 * @param serialNumber
269 * @throws IOException
270 * @throws NotAuthorizedException
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);
282 * Sets the Pool Temperature Setpoint
284 * @param serialNumber
286 * @param poolSetpoint
288 * @throws IOException
289 * @throws NotAuthorizedException
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);
301 * Sends a OneTouch set command
307 * @throws IOException
308 * @throws NotAuthorizedException
310 public OneTouch[] oneTouchSetCommand(@Nullable String serial, @Nullable String sessionID, String oneTouchID)
311 throws IOException, NotAuthorizedException {
312 return oneTouchCommand(serial, sessionID, "set_" + oneTouchID);
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);
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);
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);
337 private UriBuilder baseURI() {
338 return UriBuilder.fromUri(IAQUALINK_BASE_URL).queryParam("actionID", "command");
347 * @throws IOException
348 * @throws NotAuthorizedException
350 private <T> T getAqualinkObject(URI uri, Type typeOfT) throws IOException, NotAuthorizedException {
351 return Objects.requireNonNull(gson.fromJson(getRequest(uri), typeOfT));
358 * @throws IOException
359 * @throws NotAuthorizedException
361 private String getRequest(URI uri) throws IOException, NotAuthorizedException {
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) //
370 logger.trace("Response {}", response);
371 if (response.getStatus() == HttpStatus.UNAUTHORIZED_401) {
372 throw new NotAuthorizedException(response.getReason());
374 if (response.getStatus() != HttpStatus.OK_200) {
375 throw new IOException(response.getReason());
377 return response.getContentAsString();
378 } catch (InterruptedException | TimeoutException | ExecutionException | JsonParseException e) {
379 throw new IOException(e);
383 /////////////// .........Here be dragons...../////////////////////////
385 class HomeDeserializer implements JsonDeserializer<Home> {
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());
400 home.add("serialized_map", serializedMap);
401 return gsonInternal.fromJson(home, Home.class);
403 throw new JsonParseException("Invalid structure for Home class");
407 class OneTouchDeserializer implements JsonDeserializer<OneTouch[]> {
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());
427 list.add(Objects.requireNonNull(gsonInternal.fromJson(oneTouchJson, OneTouch.class)));
433 return list.toArray(new OneTouch[list.size()]);
437 class AuxDeserializer implements JsonDeserializer<Auxiliary[]> {
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());
457 list.add(Objects.requireNonNull(gsonInternal.fromJson(auxJson, Auxiliary.class)));
463 return list.toArray(new Auxiliary[list.size()]);